mirror of
https://github.com/marcredhat/SIEM-toolkit-patched
synced 2026-06-08 12:33:51 +00:00
4df8e844e5
End-to-end workflow that turns SigmaHQ rules into SDL Scheduled custom-detection rules: 1. SIEM-toolkit provides the coverage map to find what's thin -- MITRE ATT&CK heatmap across all detection library rules, rule firing status (active vs never-fired). 2. Pick Sigma rules (https://github.com/SigmaHQ/sigma) that target those tactics. 3. Convert the Sigma rules to PowerQuery with pysigma-backend-sentinelone-pq. 4. Smoke-test against your tenant's /api/powerQuery, deploy via /web/api/v2.1/cloud-detection/rules as Scheduled PQ rules in Draft. 5. Re-running on a different tenant is just re-pointing the credentials -- the converted .pq bodies travel as-is. Files: README_sigma_pipeline.md full workflow doc recommend_sigma_imports.py coverage-map reader -> rule shortlist probe_wel_schema.py WEL parser field discovery convert_test_deploy_sigma.py pick + convert + 3 variants + deploy fixup_rules_6_7.py OriginalFileName pre-processor run_sigma_on_tenant.py redeploy already-converted bodies verify_rule_exists_via_put.py PUT-existence test (RBAC workaround) verify_deployed_sigma_rules.py RBAC visibility diagnostic tenant_config.example.json credentials template (gitignored real one) Each converted rule emits three PowerQuery variants: <stem>.pq faithful (S1 DV schema) <stem>.relaxed.pq drops endpoint.os + event.type clauses <stem>.wel.pq rewritten onto microsoft_windows_eventlog-latest All scripts read credentials from tenant_config.json (or the SIEM_TOOLKIT_CONFIG env var), discover the target site_id at runtime, and persist deployed rule IDs to deployed_rule_ids.json so the verify scripts work without hardcoded IDs.
138 lines
5.3 KiB
Python
138 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
verify_deployed_sigma_rules.py (formerly _v3)
|
|
|
|
Diagnostic for the RBAC visibility quirk: when a service-user role has
|
|
`cloudDetectionRulesCreateEdit` but not `cloudDetectionRulesView`, POST
|
|
succeeds and returns rule IDs, but GET /rules silently hides those rules.
|
|
|
|
This script probes several scope-filter variants to characterise what
|
|
the token CAN see:
|
|
- direct GET /rules/{id}
|
|
- list with ?ids=<csv>
|
|
- list with siteIds=, accountIds=, tenant=true, no scope
|
|
- list with queryType= filter
|
|
|
|
Reads tenant credentials from tenant_config.json and the rule IDs from
|
|
deployed_rule_ids.json (both next to this script). Set SIEM_TOOLKIT_CONFIG
|
|
or DEPLOYED_IDS_FILE env vars to override.
|
|
"""
|
|
from __future__ import annotations
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
HERE = pathlib.Path(__file__).resolve().parent
|
|
_CFG_PATH = os.environ.get("SIEM_TOOLKIT_CONFIG",
|
|
str(HERE / "tenant_config.json"))
|
|
CFG = json.load(open(_CFG_PATH))
|
|
BASE = CFG["S1_CONSOLE_URL"].rstrip("/")
|
|
TOK = CFG["S1_CONSOLE_API_TOKEN"].rstrip(".")
|
|
|
|
_IDS_PATH = pathlib.Path(os.environ.get(
|
|
"DEPLOYED_IDS_FILE", str(HERE / "deployed_rule_ids.json")))
|
|
if not _IDS_PATH.exists():
|
|
raise SystemExit(f"{_IDS_PATH} not found. "
|
|
f"Run convert_test_deploy_sigma.py --deploy first.")
|
|
_STATE = json.loads(_IDS_PATH.read_text())
|
|
SITE = _STATE.get("site_id") or os.environ.get("SITE_ID") or ""
|
|
DEPLOYED_IDS = [r["rule_id"] for r in (_STATE.get("rules") or [])]
|
|
|
|
|
|
def get_json(path: str):
|
|
req = urllib.request.Request(f"{BASE}{path}")
|
|
req.add_header("Authorization", f"ApiToken {TOK}")
|
|
req.add_header("Accept", "application/json")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
return r.status, json.loads(r.read())
|
|
except urllib.error.HTTPError as e:
|
|
try:
|
|
body = json.loads(e.read())
|
|
except Exception:
|
|
body = {"_raw": "(non-json)"}
|
|
return e.code, body
|
|
|
|
|
|
def main() -> int:
|
|
print(f"\n{'='*78}\n Verify deployed rules via `ids=` filter\n"
|
|
f"{'='*78}\n Tenant : {BASE}\n Site : {SITE or '(unset)'}\n"
|
|
f" IDs : {len(DEPLOYED_IDS)} rules from {_IDS_PATH.name}\n")
|
|
|
|
# --- 1. token / user identity -----------------------------------
|
|
print("--- Step 1: token identity -------------------------------------")
|
|
code, d = get_json("/web/api/v2.1/users/api-token-details")
|
|
if code == 200:
|
|
data = d.get("data") or {}
|
|
print(f" user : {data.get('email') or data.get('fullName')}")
|
|
print(f" scope : {data.get('scope')}")
|
|
print(f" scope id : {data.get('scopeId')}")
|
|
print(f" expires : {data.get('expiresAt') or 'never'}")
|
|
else:
|
|
# Service-user JWT often can't introspect itself
|
|
code2, d2 = get_json("/web/api/v2.1/user")
|
|
if code2 == 200:
|
|
data = d2.get("data") or {}
|
|
print(f" user : {data.get('email')}")
|
|
print(f" scope : {data.get('scope')}")
|
|
else:
|
|
print(f" HTTP {code} / {code2} cannot introspect token "
|
|
"(common for service-user JWTs)")
|
|
|
|
if not DEPLOYED_IDS:
|
|
print(" No deployed rule IDs to verify.")
|
|
return 0
|
|
|
|
# --- 2. list with ids= filter, NO scope filter ------------------
|
|
print("\n--- Step 2: list with `ids=<csv>` (no scope filter) -----------")
|
|
ids = ",".join(DEPLOYED_IDS)
|
|
code, d = get_json(f"/web/api/v2.1/cloud-detection/rules?ids={ids}")
|
|
if code != 200:
|
|
print(f" HTTP {code} {json.dumps(d)[:300]}")
|
|
else:
|
|
rules = d.get("data") or []
|
|
print(f" Returned : {len(rules)} of {len(DEPLOYED_IDS)} requested")
|
|
for r in rules:
|
|
scope = (((r.get("scope") or {})
|
|
or {}).get("scopeName") or
|
|
r.get("siteName") or r.get("accountName") or "?")
|
|
print(f" id={r.get('id')} status={r.get('status'):<10} "
|
|
f"scope={scope} name={(r.get('name') or '')[:65]}")
|
|
|
|
# --- 3. list ids= AND siteIds= ----------------------------------
|
|
print("\n--- Step 3: list with `ids=` AND `siteIds=` -------------------")
|
|
code, d = get_json(
|
|
f"/web/api/v2.1/cloud-detection/rules?ids={ids}&siteIds={SITE}")
|
|
if code != 200:
|
|
print(f" HTTP {code} {json.dumps(d)[:300]}")
|
|
else:
|
|
print(f" Returned : {len(d.get('data') or [])} of "
|
|
f"{len(DEPLOYED_IDS)}")
|
|
|
|
# --- 4. list all visible scheduled rules without scope ----------
|
|
print("\n--- Step 4: list with queryType= filter ---------------------")
|
|
code, d = get_json(
|
|
"/web/api/v2.1/cloud-detection/rules"
|
|
"?queryType=scheduled&limit=200")
|
|
if code != 200:
|
|
print(f" HTTP {code} {json.dumps(d)[:300]}")
|
|
else:
|
|
rules = d.get("data") or []
|
|
sigma = [r for r in rules
|
|
if "[Sigma->PQ]" in (r.get("name") or "")]
|
|
print(f" visible scheduled rules : {len(rules)}")
|
|
print(f" of which [Sigma->PQ] : {len(sigma)}")
|
|
for r in sigma:
|
|
print(f" id={r.get('id')} status={r.get('status'):<10} "
|
|
f"{(r.get('name') or '')[:70]}")
|
|
|
|
print(f"\n Console:\n {BASE}/#/cloud-detection/rules\n")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|