Upload files to "out"
This commit is contained in:
commit
6a85b7d3f0
9
out/.env.example
Normal file
9
out/.env.example
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Splunk admin password used on FIRST boot (persists in splunk-etc/var volumes)
|
||||||
|
SPLUNK_PASSWORD=Str0ngP@ss!9
|
||||||
|
|
||||||
|
# HEC token the seeder/curl will use (already accepted by the Splunk container)
|
||||||
|
SPLUNK_HEC_TOKEN=dev-0123456789abcdef
|
||||||
|
|
||||||
|
# Azure (optional; only needed if SINK=blob or blob+sb)
|
||||||
|
AZURE_STORAGE_CONNECTION_STRING=
|
||||||
|
AZURE_SERVICEBUS_CONNECTION_STRING=
|
||||||
174
out/agent_runner.py
Normal file
174
out/agent_runner.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import os, glob, json, ujson, gzip, pathlib, re
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
||||||
|
from langchain_community.vectorstores import FAISS
|
||||||
|
from langchain_core.documents import Document
|
||||||
|
from langchain.tools import Tool
|
||||||
|
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
||||||
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||||
|
|
||||||
|
#export OPENAI_API_KEY="sk-..."
|
||||||
|
#SET API KEY^
|
||||||
|
|
||||||
|
# ---------- Config ----------
|
||||||
|
MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini")
|
||||||
|
EMB_MODEL = os.getenv("EMB_MODEL", "text-embedding-3-small")
|
||||||
|
CHUNK_DIR = os.getenv("CHUNK_DIR", "./out") # poller file sink
|
||||||
|
BLOB_DIR = os.getenv("BLOB_DIR", "") # optional local mirror of blobs
|
||||||
|
TOP_K = int(os.getenv("TOP_K", "12"))
|
||||||
|
|
||||||
|
# ---------- Load JSONL chunk files ----------
|
||||||
|
def _iter_chunk_files() -> List[pathlib.Path]:
|
||||||
|
paths = []
|
||||||
|
if CHUNK_DIR and pathlib.Path(CHUNK_DIR).exists():
|
||||||
|
paths += [pathlib.Path(p) for p in glob.glob(f"{CHUNK_DIR}/chunk_*.jsonl*")]
|
||||||
|
if BLOB_DIR and pathlib.Path(BLOB_DIR).exists():
|
||||||
|
paths += [pathlib.Path(p) for p in glob.glob(f"{BLOB_DIR}/**/chunk_*.jsonl*", recursive=True)]
|
||||||
|
# newest first
|
||||||
|
return sorted(paths, key=lambda p: p.stat().st_mtime, reverse=True)
|
||||||
|
|
||||||
|
def _read_jsonl(path: pathlib.Path) -> List[Dict[str, Any]]:
|
||||||
|
data = path.read_bytes()
|
||||||
|
if path.suffix == ".gz":
|
||||||
|
data = gzip.decompress(data)
|
||||||
|
lines = data.splitlines()
|
||||||
|
out = []
|
||||||
|
for ln in lines:
|
||||||
|
if not ln.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
out.append(ujson.loads(ln))
|
||||||
|
except Exception:
|
||||||
|
# tolerate partial/corrupt lines
|
||||||
|
continue
|
||||||
|
return out
|
||||||
|
|
||||||
|
# Accept either raw events or HEC-shaped {"event": {...}}
|
||||||
|
def _normalize_event(rec: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
evt = rec.get("event", rec)
|
||||||
|
# Ensure strings for some fields if needed
|
||||||
|
return evt
|
||||||
|
|
||||||
|
def _evt_to_text(evt: Dict[str, Any]) -> str:
|
||||||
|
# Compact text for embedding/RAG
|
||||||
|
parts = []
|
||||||
|
for k in ["event_type","transaction_id","step","status","importo","divisa","istantaneo",
|
||||||
|
"spese_commissioni","causale","data_pagamento","iban_origin_masked","iban_dest_masked",
|
||||||
|
"vop_check","vop_score","bic_swift","latency_ms","device","os","browser","geo"]:
|
||||||
|
v = evt.get(k)
|
||||||
|
if v is not None:
|
||||||
|
parts.append(f"{k}={v}")
|
||||||
|
return "bonifico | " + " | ".join(parts)
|
||||||
|
|
||||||
|
# ---------- Build vector store ----------
|
||||||
|
def build_vectorstore(limit_files: int = 20):
|
||||||
|
files = _iter_chunk_files()[:limit_files]
|
||||||
|
if not files:
|
||||||
|
raise RuntimeError("No chunk files found; set CHUNK_DIR or BLOB_DIR")
|
||||||
|
docs = []
|
||||||
|
meta_index = []
|
||||||
|
for fp in files:
|
||||||
|
rows = _read_jsonl(fp)
|
||||||
|
for rec in rows:
|
||||||
|
evt = _normalize_event(rec)
|
||||||
|
txt = _evt_to_text(evt)
|
||||||
|
docs.append(Document(page_content=txt, metadata={"file": fp.name, **{k: evt.get(k) for k in ("transaction_id","step","status")}}))
|
||||||
|
meta_index.append(evt)
|
||||||
|
embeddings = OpenAIEmbeddings(model=EMB_MODEL)
|
||||||
|
vs = FAISS.from_documents(docs, embeddings)
|
||||||
|
return vs, meta_index
|
||||||
|
|
||||||
|
# ---------- Handy utilities (tools) ----------
|
||||||
|
def stats_tool_impl(query: str = "") -> str:
|
||||||
|
"""
|
||||||
|
Return quick stats from latest chunks. Query supports simple filters like:
|
||||||
|
'status:rejected min_amount:10000 step:esito'
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
vs, meta_index = build_vectorstore()
|
||||||
|
# simple filter pass
|
||||||
|
min_amount, step, status = 0.0, None, None
|
||||||
|
m = re.search(r"min_amount:(\d+(\.\d+)?)", query); min_amount = float(m.group(1)) if m else 0.0
|
||||||
|
m = re.search(r"step:(\w+)", query); step = m.group(1) if m else None
|
||||||
|
m = re.search(r"status:(\w+)", query); status = m.group(1) if m else None
|
||||||
|
|
||||||
|
total = 0; rej = 0; amt_sum = 0.0; hi = 0.0; hi_tx = None
|
||||||
|
for evt in meta_index:
|
||||||
|
try:
|
||||||
|
amt = float(evt.get("importo", 0))
|
||||||
|
except Exception:
|
||||||
|
amt = 0.0
|
||||||
|
if amt < min_amount: continue
|
||||||
|
if step and evt.get("step") != step: continue
|
||||||
|
if status and evt.get("status") != status: continue
|
||||||
|
total += 1
|
||||||
|
amt_sum += amt
|
||||||
|
if evt.get("status") == "rejected": rej += 1
|
||||||
|
if amt > hi: hi, hi_tx = amt, evt.get("transaction_id")
|
||||||
|
rr = f"events={total}, rejected={rej}, rejection_rate={round(rej/max(total,1),3)}, total_amount={round(amt_sum,2)}, max_amount={hi} (tx={hi_tx})"
|
||||||
|
return rr
|
||||||
|
|
||||||
|
def retrieve_tool_impl(question: str) -> str:
|
||||||
|
"""Semantic retrieve top-K log snippets related to the question."""
|
||||||
|
vs, _ = build_vectorstore()
|
||||||
|
docs = vs.similarity_search(question, k=TOP_K)
|
||||||
|
lines = [f"[{i+1}] {d.page_content}" for i,d in enumerate(docs)]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def raw_sample_tool_impl(n: int = 5) -> str:
|
||||||
|
"""Return n raw events (JSON) from the newest chunks."""
|
||||||
|
files = _iter_chunk_files()
|
||||||
|
out = []
|
||||||
|
for fp in files:
|
||||||
|
for rec in _read_jsonl(fp):
|
||||||
|
out.append(json.dumps(_normalize_event(rec), ensure_ascii=False))
|
||||||
|
if len(out) >= n: break
|
||||||
|
if len(out) >= n: break
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
# ---------- Build the agent ----------
|
||||||
|
def build_agent():
|
||||||
|
llm = ChatOpenAI(model=MODEL, temperature=0.2)
|
||||||
|
|
||||||
|
tools = [
|
||||||
|
Tool(name="get_stats",
|
||||||
|
func=stats_tool_impl,
|
||||||
|
description="Quick stats over recent events. Usage: pass a filter string like 'status:rejected min_amount:10000 step:esito'."),
|
||||||
|
Tool(name="retrieve_similar",
|
||||||
|
func=retrieve_tool_impl,
|
||||||
|
description="Semantic search over logs. Pass a natural-language question about bonifico logs."),
|
||||||
|
Tool(name="raw_samples",
|
||||||
|
func=raw_sample_tool_impl,
|
||||||
|
description="Return a few raw JSON events to inspect fields.")
|
||||||
|
]
|
||||||
|
|
||||||
|
system = """You are a payments log analyst. Use the tools to inspect recent Splunk-derived logs for 'bonifico' events.
|
||||||
|
- Prefer 'get_stats' for quick metrics (rejection rate, totals).
|
||||||
|
- Use 'retrieve_similar' to pull relevant examples before concluding.
|
||||||
|
- When asked for anomalies, treat as suspicious: rejected EUR transfers >= 10,000, 'vop_no_match', invalid IBAN/BIC, unusual spikes.
|
||||||
|
Return a short, structured report with: Findings, Evidence (IDs/fields), and Recommended actions."""
|
||||||
|
|
||||||
|
prompt = ChatPromptTemplate.from_messages([
|
||||||
|
("system", system),
|
||||||
|
MessagesPlaceholder("chat_history"),
|
||||||
|
("human", "{input}"),
|
||||||
|
MessagesPlaceholder("agent_scratchpad"),
|
||||||
|
])
|
||||||
|
|
||||||
|
agent = create_tool_calling_agent(llm, tools, prompt)
|
||||||
|
executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)
|
||||||
|
return executor
|
||||||
|
|
||||||
|
def run_default_question():
|
||||||
|
agent = build_agent()
|
||||||
|
question = (
|
||||||
|
"Scan the latest chunks. List any anomalies (e.g., rejected EUR >= 10000, vop_no_match, invalid IBAN/BIC). "
|
||||||
|
"Give a brief summary and next steps."
|
||||||
|
)
|
||||||
|
out = agent.invoke({"input": question, "chat_history": []})
|
||||||
|
print("\n=== AGENT OUTPUT ===\n", out["output"])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_default_question()
|
||||||
62
out/compose.yaml
Normal file
62
out/compose.yaml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
splunk:
|
||||||
|
image: splunk/splunk:9.4.2
|
||||||
|
container_name: splunk
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8000:8000" # Splunk Web
|
||||||
|
- "8088:8088" # HEC
|
||||||
|
- "8089:8089" # Management API
|
||||||
|
environment:
|
||||||
|
SPLUNK_START_ARGS: --accept-license
|
||||||
|
SPLUNK_PASSWORD: ${SPLUNK_PASSWORD}
|
||||||
|
SPLUNK_HEC_TOKEN: ${SPLUNK_HEC_TOKEN}
|
||||||
|
volumes:
|
||||||
|
- splunk-etc:/opt/splunk/etc
|
||||||
|
- splunk-var:/opt/splunk/var
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -sk https://localhost:8089/services/server/info | grep -q version"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 30
|
||||||
|
|
||||||
|
poller:
|
||||||
|
build:
|
||||||
|
context: ./poller
|
||||||
|
container_name: splunk-poller
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
splunk:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
# --- Splunk connection ---
|
||||||
|
SPLUNK_HOST: splunk
|
||||||
|
SPLUNK_PORT: "8089"
|
||||||
|
SPLUNK_USER: admin
|
||||||
|
SPLUNK_PW: ${SPLUNK_PASSWORD}
|
||||||
|
SPLUNK_VERIFY_SSL: "false" # self-signed in container
|
||||||
|
# --- What to read ---
|
||||||
|
SPLUNK_INDEX: intesa_payments
|
||||||
|
SPLUNK_SOURCETYPE: intesa:bonifico
|
||||||
|
INITIAL_LOOKBACK: -24h@h
|
||||||
|
# --- Polling / chunking ---
|
||||||
|
SLEEP_SECONDS: "60"
|
||||||
|
MAX_CHUNK_BYTES: "1800000"
|
||||||
|
CREATE_INDEX_IF_MISSING: "true"
|
||||||
|
# --- Sink selection: file | blob | blob+sb ---
|
||||||
|
SINK: file
|
||||||
|
OUTDIR: /app/out
|
||||||
|
# --- Azure (only if using blob / blob+sb) ---
|
||||||
|
AZURE_STORAGE_CONNECTION_STRING: ${AZURE_STORAGE_CONNECTION_STRING:-}
|
||||||
|
AZURE_STORAGE_CONTAINER: bank-logs
|
||||||
|
AZURE_SERVICEBUS_CONNECTION_STRING: ${AZURE_SERVICEBUS_CONNECTION_STRING:-}
|
||||||
|
AZURE_SERVICEBUS_QUEUE: log-chunks
|
||||||
|
AZURE_COMPRESS: "true"
|
||||||
|
volumes:
|
||||||
|
- ./out:/app/out
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
splunk-etc:
|
||||||
|
splunk-var:
|
||||||
191
out/offline_analyzer.py
Normal file
191
out/offline_analyzer.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os, glob, json, gzip, time, pathlib, math, statistics as stats
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
CHUNK_DIR = os.getenv("CHUNK_DIR", "./out")
|
||||||
|
REPORT_DIR = pathlib.Path(os.getenv("REPORT_DIR", "./reports"))
|
||||||
|
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _iter_files():
|
||||||
|
paths = sorted(glob.glob(f"{CHUNK_DIR}/chunk_*.jsonl*"))
|
||||||
|
for p in paths:
|
||||||
|
yield pathlib.Path(p)
|
||||||
|
|
||||||
|
def _read_jsonl(p: pathlib.Path):
|
||||||
|
data = p.read_bytes()
|
||||||
|
if p.suffix == ".gz":
|
||||||
|
data = gzip.decompress(data)
|
||||||
|
for line in data.splitlines():
|
||||||
|
if not line.strip(): continue
|
||||||
|
try:
|
||||||
|
rec = json.loads(line)
|
||||||
|
yield rec.get("event", rec) # accept HEC shape or plain
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
def _to_float(x, default=0.0):
|
||||||
|
try:
|
||||||
|
if isinstance(x, (int, float)): return float(x)
|
||||||
|
if isinstance(x, str): return float(x.strip().replace(",", ""))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
|
||||||
|
def _boolish(x):
|
||||||
|
if isinstance(x, bool): return x
|
||||||
|
if isinstance(x, str): return x.lower() in {"true","1","yes"}
|
||||||
|
return False
|
||||||
|
|
||||||
|
def analyze(events):
|
||||||
|
total = 0
|
||||||
|
by_status = {}
|
||||||
|
by_step = {}
|
||||||
|
total_amt = 0.0
|
||||||
|
amounts = []
|
||||||
|
inst_count = 0
|
||||||
|
latencies = {"compila": [], "conferma": [], "esito": []}
|
||||||
|
rejections = []
|
||||||
|
vop_flags = []
|
||||||
|
by_minute = {}
|
||||||
|
|
||||||
|
anomalies = [] # collected dicts
|
||||||
|
|
||||||
|
for e in events:
|
||||||
|
total += 1
|
||||||
|
st = str(e.get("status","")).lower() or "unknown"
|
||||||
|
step = str(e.get("step","")).lower() or "unknown"
|
||||||
|
by_status[st] = by_status.get(st,0)+1
|
||||||
|
by_step[step] = by_step.get(step,0)+1
|
||||||
|
|
||||||
|
amt = _to_float(e.get("importo"))
|
||||||
|
total_amt += amt
|
||||||
|
amounts.append(amt)
|
||||||
|
|
||||||
|
if _boolish(e.get("istantaneo")): inst_count += 1
|
||||||
|
|
||||||
|
lat = _to_float(e.get("latency_ms"), default=None)
|
||||||
|
if lat is not None and step in latencies: latencies[step].append(lat)
|
||||||
|
|
||||||
|
# time bucket (minute)
|
||||||
|
ts = e.get("data_pagamento") or e.get("_time")
|
||||||
|
if isinstance(ts, str) and len(ts)>=16:
|
||||||
|
key = ts[:16] # 'YYYY-MM-DDTHH:MM'
|
||||||
|
by_minute[key] = by_minute.get(key, 0) + 1
|
||||||
|
|
||||||
|
# collect rejection info
|
||||||
|
if st == "rejected":
|
||||||
|
rejections.append(e)
|
||||||
|
|
||||||
|
# vop flags
|
||||||
|
vop = (e.get("vop_check") or "").lower()
|
||||||
|
if vop in {"no_match","close_match"}:
|
||||||
|
vop_flags.append({"transaction_id": e.get("transaction_id"),
|
||||||
|
"vop_check": vop, "vop_score": e.get("vop_score"),
|
||||||
|
"importo": amt, "divisa": e.get("divisa")})
|
||||||
|
|
||||||
|
# --- anomaly rules ---
|
||||||
|
# A1: rejected EUR >= 10k
|
||||||
|
if st=="rejected" and (e.get("divisa")=="EUR") and amt >= 10000:
|
||||||
|
anomalies.append({"rule":"A1_rejected_high_value_eur",
|
||||||
|
"transaction_id": e.get("transaction_id"),
|
||||||
|
"amount": amt,
|
||||||
|
"divisa": e.get("divisa"),
|
||||||
|
"iban_dest_masked": e.get("iban_dest_masked"),
|
||||||
|
"causale": e.get("causale")})
|
||||||
|
|
||||||
|
# A2: VOP no_match or low score & amount >= 5k
|
||||||
|
vop_score = _to_float(e.get("vop_score"), default=None)
|
||||||
|
if (vop=="no_match") or (vop=="close_match" and vop_score is not None and vop_score < 0.75 and amt>=5000):
|
||||||
|
anomalies.append({"rule":"A2_vop_flagged",
|
||||||
|
"transaction_id": e.get("transaction_id"),
|
||||||
|
"vop_check": vop,
|
||||||
|
"vop_score": vop_score,
|
||||||
|
"amount": amt})
|
||||||
|
|
||||||
|
# A3: high latency per step
|
||||||
|
thr = {"compila":600, "conferma":800, "esito":900}.get(step, 900)
|
||||||
|
if lat is not None and lat > thr:
|
||||||
|
anomalies.append({"rule":"A3_high_latency",
|
||||||
|
"transaction_id": e.get("transaction_id"),
|
||||||
|
"step": step, "latency_ms": lat})
|
||||||
|
|
||||||
|
# A4: instant transfer but pending/rejected
|
||||||
|
if _boolish(e.get("istantaneo")) and st in {"pending","rejected"} and step=="esito":
|
||||||
|
anomalies.append({"rule":"A4_instant_not_accepted",
|
||||||
|
"transaction_id": e.get("transaction_id"),
|
||||||
|
"status": st, "amount": amt})
|
||||||
|
|
||||||
|
# spike detection (very simple): minute counts > mean+3*std
|
||||||
|
if by_minute:
|
||||||
|
counts = list(by_minute.values())
|
||||||
|
mu = stats.mean(counts)
|
||||||
|
sd = stats.pstdev(counts) if len(counts)>1 else 0.0
|
||||||
|
for minute, c in by_minute.items():
|
||||||
|
if sd>0 and c > mu + 3*sd:
|
||||||
|
anomalies.append({"rule":"A5_volume_spike", "minute": minute, "count": c, "mu": round(mu,2), "sd": round(sd,2)})
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"events": total,
|
||||||
|
"accepted": by_status.get("accepted",0),
|
||||||
|
"pending": by_status.get("pending",0),
|
||||||
|
"rejected": by_status.get("rejected",0),
|
||||||
|
"rejection_rate": round(by_status.get("rejected",0)/max(total,1), 4),
|
||||||
|
"total_amount": round(total_amt,2),
|
||||||
|
"avg_amount": round((sum(amounts)/len(amounts)) if amounts else 0.0, 2),
|
||||||
|
"instant_share": round(inst_count/max(total,1), 4),
|
||||||
|
"by_step": by_step,
|
||||||
|
"latency_avg_ms": {k:(round(sum(v)/len(v),1) if v else None) for k,v in latencies.items()},
|
||||||
|
"vop_flags": len(vop_flags),
|
||||||
|
"spike_minutes": len([a for a in anomalies if a["rule"]=="A5_volume_spike"]),
|
||||||
|
"anomaly_count": len(anomalies),
|
||||||
|
}
|
||||||
|
return summary, anomalies
|
||||||
|
|
||||||
|
def load_all_events():
|
||||||
|
files = list(_iter_files())
|
||||||
|
if not files:
|
||||||
|
raise SystemExit(f"No chunk files in {CHUNK_DIR}.")
|
||||||
|
events = []
|
||||||
|
for p in files:
|
||||||
|
events.extend(_read_jsonl(p))
|
||||||
|
return events
|
||||||
|
|
||||||
|
def write_reports(summary, anomalies):
|
||||||
|
ts = int(time.time())
|
||||||
|
md_path = REPORT_DIR / f"report_{ts}.md"
|
||||||
|
js_path = REPORT_DIR / f"anomalies_{ts}.json"
|
||||||
|
# Markdown
|
||||||
|
md = []
|
||||||
|
md.append(f"# Bonifico Log Analysis — {datetime.now(timezone.utc).isoformat()}")
|
||||||
|
md.append("")
|
||||||
|
md.append("## Summary")
|
||||||
|
for k,v in summary.items():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
md.append(f"- **{k}**: `{json.dumps(v, ensure_ascii=False)}`")
|
||||||
|
else:
|
||||||
|
md.append(f"- **{k}**: `{v}`")
|
||||||
|
md.append("")
|
||||||
|
md.append("## Anomalies")
|
||||||
|
if not anomalies:
|
||||||
|
md.append("_None detected by rules._")
|
||||||
|
else:
|
||||||
|
for a in anomalies[:200]:
|
||||||
|
md.append(f"- `{a['rule']}` — `{json.dumps(a, ensure_ascii=False)}`")
|
||||||
|
if len(anomalies) > 200:
|
||||||
|
md.append(f"... and {len(anomalies)-200} more.")
|
||||||
|
md_path.write_text("\n".join(md), encoding="utf-8")
|
||||||
|
# JSON
|
||||||
|
js_path.write_text(json.dumps({"summary":summary,"anomalies":anomalies}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
print(f"Wrote {md_path}")
|
||||||
|
print(f"Wrote {js_path}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
evts = load_all_events()
|
||||||
|
summary, anomalies = analyze(evts)
|
||||||
|
write_reports(summary, anomalies)
|
||||||
|
# also print a short console digest
|
||||||
|
print("\nDigest:", json.dumps({
|
||||||
|
"events": summary["events"],
|
||||||
|
"rejection_rate": summary["rejection_rate"],
|
||||||
|
"anomaly_count": summary["anomaly_count"]
|
||||||
|
}))
|
||||||
65
out/sampleLogs.txt
Normal file
65
out/sampleLogs.txt
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#Cli preset parameters
|
||||||
|
HEC_URL="https://localhost:8088/services/collector/event"
|
||||||
|
HEC_TOKEN="dev-0123456789abcdef"
|
||||||
|
INDEX="intesa_payments"
|
||||||
|
SOURCETYPE="intesa:bonifico"
|
||||||
|
|
||||||
|
#Cli script for generating logs
|
||||||
|
gen_iban(){ local d=""; for _ in $(seq 1 25); do d="${d}$((RANDOM%10))"; done; echo "IT${d}"; }
|
||||||
|
mask_iban(){ local i="$1"; local pre="${i:0:6}"; local suf="${i: -4}"; local n=$(( ${#i}-10 )); printf "%s%0.s*" "$pre" $(seq 1 $n); echo -n "$suf"; }
|
||||||
|
rand_amount(){ awk 'BEGIN{srand(); printf "%.2f", 5+rand()*14995}'; }
|
||||||
|
rand_bool_str(){ if ((RANDOM%2)); then echo "true"; else echo "false"; fi; }
|
||||||
|
pick(){ local a=("$@"); echo "${a[$RANDOM%${#a[@]}]}"; }
|
||||||
|
|
||||||
|
spese=(SHA OUR BEN)
|
||||||
|
divise=(EUR EUR EUR EUR USD GBP)
|
||||||
|
statuses=(accepted pending rejected)
|
||||||
|
|
||||||
|
for tx in {1..20}; do
|
||||||
|
txid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || openssl rand -hex 16)
|
||||||
|
t0=$(date -u +%s); t1=$((t0+1)); t2=$((t1+2))
|
||||||
|
iso0=$(date -u -d @$t0 +%FT%T.%6NZ)
|
||||||
|
iso1=$(date -u -d @$t1 +%FT%T.%6NZ)
|
||||||
|
iso2=$(date -u -d @$t2 +%FT%T.%6NZ)
|
||||||
|
|
||||||
|
src=$(gen_iban); dst=$(gen_iban)
|
||||||
|
srcm=$(mask_iban "$src"); dstm=$(mask_iban "$dst")
|
||||||
|
amt=$(rand_amount)
|
||||||
|
dv=$(pick "${divise[@]}")
|
||||||
|
inst=$(rand_bool_str)
|
||||||
|
sp=$(pick "${spese[@]}")
|
||||||
|
final=$(pick "${statuses[@]}")
|
||||||
|
|
||||||
|
send() {
|
||||||
|
local when="$1" iso="$2" step="$3" status="$4"
|
||||||
|
curl -sk "$HEC_URL" \
|
||||||
|
-H "Authorization: Splunk $HEC_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d @- <<JSON
|
||||||
|
{
|
||||||
|
"time": $when,
|
||||||
|
"host": "seed.cli",
|
||||||
|
"source": "cli_for_loop",
|
||||||
|
"sourcetype": "$SOURCETYPE",
|
||||||
|
"index": "$INDEX",
|
||||||
|
"event": {
|
||||||
|
"event_type": "bonifico",
|
||||||
|
"transaction_id": "$txid",
|
||||||
|
"step": "$step",
|
||||||
|
"iban_origin_masked": "$srcm",
|
||||||
|
"iban_dest_masked": "$dstm",
|
||||||
|
"importo": "$amt",
|
||||||
|
"divisa": "$dv",
|
||||||
|
"istantaneo": "$inst",
|
||||||
|
"data_pagamento": "$iso",
|
||||||
|
"spese_commissioni": "$sp",
|
||||||
|
"causale": "TEST SEED",
|
||||||
|
"status": "$status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
send "$t0" "$iso0" "compila" "in_progress"
|
||||||
|
send "$t1" "$iso1" "conferma" "in_progress"
|
||||||
|
send "$t2" "$iso2" "esito" "$final"
|
||||||
|
done
|
||||||
Loading…
x
Reference in New Issue
Block a user