loki-mode 7.49.0 → 7.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/prd-analyzer.py +215 -1
- package/autonomy/prd-checklist.sh +315 -0
- package/autonomy/run.sh +124 -0
- package/autonomy/spec-interrogation.sh +224 -4
- package/autonomy/spec.sh +25 -16
- package/autonomy/verify.sh +108 -26
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +202 -21
- package/docs/INSTALLATION.md +2 -2
- package/docs/siem-integration.md +102 -0
- package/loki-ts/dist/loki.js +231 -230
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/invariant-checks.md +109 -0
- package/src/audit/crosslink.js +413 -0
- package/src/audit/index.js +32 -0
- package/src/observability/siem-export.js +424 -0
- package/src/policies/cost.js +270 -1
package/autonomy/verify.sh
CHANGED
|
@@ -453,11 +453,104 @@ verify_gate_static() {
|
|
|
453
453
|
}
|
|
454
454
|
|
|
455
455
|
# ---------------------------------------------------------------------------
|
|
456
|
-
# Gate: secret scan (NET-NEW).
|
|
457
|
-
#
|
|
456
|
+
# Gate: secret scan (NET-NEW).
|
|
457
|
+
#
|
|
458
|
+
# WHAT IS SCANNED:
|
|
459
|
+
# Only the files in the PR diff (merge-base(base,HEAD)..HEAD), i.e. the net
|
|
460
|
+
# change being verified. Pre-existing secrets in untouched files are NOT
|
|
461
|
+
# scanned by design: a verifier attests that THE CHANGE is safe, and blocking
|
|
462
|
+
# on legacy secrets is the #1 false-positive-fatigue failure (spec 7.2).
|
|
463
|
+
# Binary files are skipped.
|
|
464
|
+
#
|
|
465
|
+
# WHEN IT RUNS:
|
|
466
|
+
# As a gate inside `loki verify`, after generation/edits and BEFORE the change
|
|
467
|
+
# is treated as VERIFIED. A real secret here forces a non-VERIFIED verdict.
|
|
468
|
+
#
|
|
469
|
+
# GITLEAKS vs FALLBACK:
|
|
470
|
+
# - If `gitleaks` is on PATH, it scans each changed file (filesystem mode,
|
|
471
|
+
# `detect --no-git --source`). Any hit -> Critical finding for that file.
|
|
472
|
+
# - If gitleaks is NOT installed, a documented two-tier regex fallback runs
|
|
473
|
+
# (see verify_secret_scan_file). The fallback is a deterministic safety net,
|
|
474
|
+
# not a replacement for a full scanner; gitleaks is recommended for depth.
|
|
475
|
+
#
|
|
476
|
+
# BLOCKING GUARANTEE:
|
|
477
|
+
# Every detected secret is emitted as a Critical finding. The default
|
|
478
|
+
# --block-on is "critical,high" (verify_main), so verify_compute_verdict sets
|
|
479
|
+
# verdict=BLOCKED and exit=2 (VERIFY_EXIT_BLOCKED). The scan BLOCKS, it does
|
|
480
|
+
# not merely warn. (Operators who pass a narrower --block-on accept the risk.)
|
|
481
|
+
#
|
|
482
|
+
# FOLLOW-UP (deferred, honest): a true pre-WRITE hook that scans generated bytes
|
|
483
|
+
# BEFORE they touch disk would be stronger still, but it must hook every write
|
|
484
|
+
# path in run.sh (out of scope for this verify.sh slice). The gate here is the
|
|
485
|
+
# strong post-generation / pre-completion scan; the pre-write hook is tracked
|
|
486
|
+
# as a follow-up and is NOT claimed to exist.
|
|
458
487
|
#
|
|
459
488
|
# skipped = no changed files to scan.
|
|
460
489
|
# ---------------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
# Two-tier regex secret matcher for a single file. Echoes nothing; returns 0 if
|
|
492
|
+
# a high-confidence secret is found, 1 otherwise. Used ONLY by the fallback path
|
|
493
|
+
# (gitleaks absent). Kept as a standalone function so the test suite can drive it
|
|
494
|
+
# and so the two tiers are documented in one place.
|
|
495
|
+
#
|
|
496
|
+
# TIER 1 -- specific credential FORMATS. A match is conclusive on its own: the
|
|
497
|
+
# string already looks exactly like a real credential, so we do NOT apply the
|
|
498
|
+
# placeholder filter (a leaked key is a leak even if a comment says EXAMPLE).
|
|
499
|
+
#
|
|
500
|
+
# TIER 2 -- generic long quoted-value assignments (api_key="...", bearer
|
|
501
|
+
# tokens). This is a length + charset heuristic (>=16 contiguous secret-charset
|
|
502
|
+
# chars), NOT a true Shannon-entropy measure. These are false-positive magnets,
|
|
503
|
+
# so a match is only counted if the matched LINE survives the placeholder /
|
|
504
|
+
# env-reference deny filter. That keeps obvious non-secrets (your-api-key-here,
|
|
505
|
+
# REDACTED, ${API_KEY}, process.env.X) clean.
|
|
506
|
+
verify_secret_scan_file() {
|
|
507
|
+
local file="$1"
|
|
508
|
+
|
|
509
|
+
# TIER 1: specific formats. No deny filter -- a format match is a finding.
|
|
510
|
+
local tier1=(
|
|
511
|
+
'AKIA[0-9A-Z]{16}' # AWS access key id
|
|
512
|
+
'ASIA[0-9A-Z]{16}' # AWS temporary (STS) key id
|
|
513
|
+
'-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----' # PEM private key block
|
|
514
|
+
'gh[pousr]_[A-Za-z0-9]{36,}' # GitHub token (ghp_/gho_/...)
|
|
515
|
+
'github_pat_[A-Za-z0-9_]{60,}' # GitHub fine-grained PAT
|
|
516
|
+
'xox[baprs]-[A-Za-z0-9-]{10,}' # Slack token (xoxb-/xoxp-/...)
|
|
517
|
+
'sk-[A-Za-z0-9]{20,}' # OpenAI-style secret key
|
|
518
|
+
'AIza[0-9A-Za-z_-]{35}' # Google API key
|
|
519
|
+
'glpat-[A-Za-z0-9_-]{20,}' # GitLab personal access token
|
|
520
|
+
)
|
|
521
|
+
local p
|
|
522
|
+
for p in "${tier1[@]}"; do
|
|
523
|
+
# -e terminates option parsing so patterns beginning with '-' (the PEM
|
|
524
|
+
# "-----BEGIN ... PRIVATE KEY-----" block) are not mistaken for a flag.
|
|
525
|
+
if LC_ALL=C grep -Eq -e "$p" "$file" 2>/dev/null; then
|
|
526
|
+
return 0
|
|
527
|
+
fi
|
|
528
|
+
done
|
|
529
|
+
|
|
530
|
+
# Deny filter for TIER 2: a matched line is IGNORED if it is plainly a
|
|
531
|
+
# placeholder or an environment-variable reference rather than a literal.
|
|
532
|
+
# - env refs: ${VAR}, $VAR, process.env.X, os.environ/os.getenv, %VAR%
|
|
533
|
+
# - placeholders: your-/your_..., redacted, changeme, placeholder, example,
|
|
534
|
+
# dummy/sample/fake, xxxx+, <...>, runs of 4+ asterisks
|
|
535
|
+
# ("test" is deliberately NOT denied -- too common, would mask real keys)
|
|
536
|
+
local deny='(\$\{|\$[A-Za-z_]|process\.env|os\.(environ|getenv)|%[A-Za-z_]+%|your[-_]|redacted|changeme|change[-_]me|placeholder|example|dummy|sample|fake|<[^>]*>|x{4,}|\*{4,})'
|
|
537
|
+
|
|
538
|
+
# TIER 2: generic assignments. Grab matching lines, drop denied ones, count.
|
|
539
|
+
# api_key / apikey / secret / token / password / passwd / access_key, an
|
|
540
|
+
# assignment operator (= or :), then a quoted >=16-char high-entropy value.
|
|
541
|
+
local tier2='(api[_-]?key|secret|token|password|passwd|access[_-]?key|client[_-]?secret|auth)[A-Za-z0-9_]*[[:space:]]*[:=][[:space:]]*["'"'"']?[A-Za-z0-9_/+.=-]{16,}'
|
|
542
|
+
# Bearer tokens: "Bearer <>=20 high-entropy chars>".
|
|
543
|
+
local bearer='[Bb]earer[[:space:]]+[A-Za-z0-9_.\-]{20,}'
|
|
544
|
+
|
|
545
|
+
local surviving
|
|
546
|
+
surviving="$(LC_ALL=C grep -EiI "$tier2|$bearer" "$file" 2>/dev/null \
|
|
547
|
+
| LC_ALL=C grep -Eiv "$deny" 2>/dev/null)"
|
|
548
|
+
if [ -n "$surviving" ]; then
|
|
549
|
+
return 0
|
|
550
|
+
fi
|
|
551
|
+
return 1
|
|
552
|
+
}
|
|
553
|
+
|
|
461
554
|
verify_gate_secret_scan() {
|
|
462
555
|
local tree="$1"
|
|
463
556
|
local changed="$VERIFY_DIFF_NAMES"
|
|
@@ -504,17 +597,10 @@ verify_gate_secret_scan() {
|
|
|
504
597
|
return 0
|
|
505
598
|
fi
|
|
506
599
|
|
|
507
|
-
# High-confidence
|
|
508
|
-
#
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
'-----BEGIN [A-Z ]*PRIVATE KEY-----' # PEM private key
|
|
512
|
-
'gh[pousr]_[A-Za-z0-9]{36,}' # GitHub token
|
|
513
|
-
'xox[baprs]-[A-Za-z0-9-]{10,}' # Slack token
|
|
514
|
-
'sk-[A-Za-z0-9]{20,}' # OpenAI-style key
|
|
515
|
-
'AIza[0-9A-Za-z_-]{35}' # Google API key
|
|
516
|
-
)
|
|
517
|
-
|
|
600
|
+
# High-confidence two-tier matcher (verify_secret_scan_file): tier 1 specific
|
|
601
|
+
# credential formats flag unconditionally; tier 2 generic assignments flag
|
|
602
|
+
# only when the line survives the placeholder/env-ref deny filter. Conservative
|
|
603
|
+
# on purpose to limit false positives (spec Section 7.2).
|
|
518
604
|
local hits=0
|
|
519
605
|
local detail=""
|
|
520
606
|
local f
|
|
@@ -523,16 +609,12 @@ verify_gate_secret_scan() {
|
|
|
523
609
|
[ -f "$tree/$f" ] || continue
|
|
524
610
|
# Skip obvious binaries.
|
|
525
611
|
if LC_ALL=C grep -Iq . "$tree/$f" 2>/dev/null; then : ; else continue; fi
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
"Potential hardcoded secret detected in $f (matched a high-confidence credential pattern)."
|
|
533
|
-
break
|
|
534
|
-
fi
|
|
535
|
-
done
|
|
612
|
+
if verify_secret_scan_file "$tree/$f"; then
|
|
613
|
+
hits=$((hits + 1))
|
|
614
|
+
detail="${detail}${f} "
|
|
615
|
+
_verify_add_finding "Critical" "security" "deterministic:regex-secret-scan" "$f" "null" \
|
|
616
|
+
"Potential hardcoded secret detected in $f (matched a high-confidence credential pattern)."
|
|
617
|
+
fi
|
|
536
618
|
done <<<"$changed"
|
|
537
619
|
|
|
538
620
|
if [ "$hits" -eq 0 ]; then
|
|
@@ -975,9 +1057,9 @@ EOF
|
|
|
975
1057
|
# Living-spec drift gate (integration with autonomy/spec.sh).
|
|
976
1058
|
#
|
|
977
1059
|
# When .loki/spec/spec.lock exists, source the spec module and run its
|
|
978
|
-
# verify hook, which emits at most one SPEC_DRIFT finding (
|
|
979
|
-
# in the verify finding TSV shape. Records a gate row either
|
|
980
|
-
# evidence document shows the spec was checked. Fully graceful: no lock, no
|
|
1060
|
+
# verify hook, which emits at most one SPEC_DRIFT finding (High -> BLOCKED when
|
|
1061
|
+
# the spec is locked) in the verify finding TSV shape. Records a gate row either
|
|
1062
|
+
# way so the evidence document shows the spec was checked. Fully graceful: no lock, no
|
|
981
1063
|
# module, or a hook error all degrade to a skipped gate and never abort verify.
|
|
982
1064
|
# ---------------------------------------------------------------------------
|
|
983
1065
|
verify_spec_drift_gate() {
|
package/dashboard/__init__.py
CHANGED
package/dashboard/audit.py
CHANGED
|
@@ -513,30 +513,25 @@ def _file_has_integrity(log_file: str) -> bool:
|
|
|
513
513
|
return False
|
|
514
514
|
|
|
515
515
|
|
|
516
|
-
def
|
|
517
|
-
"""
|
|
516
|
+
def verify_all_logs_in_dir(audit_dir) -> dict:
|
|
517
|
+
"""Verify the entire audit chain across all rotated log files in
|
|
518
|
+
an explicit directory.
|
|
518
519
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
520
|
+
This is the directory-parameterized core of :func:`verify_all_logs`.
|
|
521
|
+
The default :func:`verify_all_logs` delegates here with the module
|
|
522
|
+
``AUDIT_DIR`` so existing callers are unaffected. The explicit-dir
|
|
523
|
+
form lets the unified cross-chain verifier (see ``src/audit/crosslink.js``)
|
|
524
|
+
validate an arbitrary audit directory without mutating module globals.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
audit_dir: Path (str or pathlib.Path) to a directory containing
|
|
528
|
+
``audit-*.jsonl`` files.
|
|
524
529
|
|
|
525
530
|
Returns:
|
|
526
|
-
|
|
527
|
-
- valid (bool): True if the entire cross-file chain is intact.
|
|
528
|
-
- files_checked (int): Count of integrity-bearing files inspected.
|
|
529
|
-
- files_skipped (int): Count of pre-integrity files skipped.
|
|
530
|
-
- entries_checked (int): Total entries verified across all files.
|
|
531
|
-
- first_tampered_file (str | None): Path to the first file
|
|
532
|
-
whose chain broke, or None if valid.
|
|
533
|
-
- first_tampered_line (int | None): 1-based line number in
|
|
534
|
-
that file where the chain broke, or None if valid.
|
|
535
|
-
- genesis_file (str | None): Path to the first integrity-bearing
|
|
536
|
-
log file (the chain's genesis on this machine), or None if
|
|
537
|
-
no integrity-bearing files exist.
|
|
531
|
+
Same shape as :func:`verify_all_logs`.
|
|
538
532
|
"""
|
|
539
|
-
|
|
533
|
+
audit_dir = Path(audit_dir)
|
|
534
|
+
if not audit_dir.exists():
|
|
540
535
|
return {"valid": True, "files_checked": 0, "files_skipped": 0,
|
|
541
536
|
"entries_checked": 0, "first_tampered_file": None,
|
|
542
537
|
"first_tampered_line": None, "genesis_file": None}
|
|
@@ -547,7 +542,7 @@ def verify_all_logs() -> dict:
|
|
|
547
542
|
# would break chain ordering and false-negative on any user who hit
|
|
548
543
|
# size-based rotation. Sort by mtime instead -- mirrors what
|
|
549
544
|
# `_cleanup_old_logs` already does at line 178.
|
|
550
|
-
files = sorted(
|
|
545
|
+
files = sorted(audit_dir.glob("audit-*.jsonl"), key=lambda p: p.stat().st_mtime)
|
|
551
546
|
prev_hash = "0" * 64
|
|
552
547
|
total_entries = 0
|
|
553
548
|
files_checked = 0
|
|
@@ -582,3 +577,189 @@ def verify_all_logs() -> dict:
|
|
|
582
577
|
"first_tampered_line": None,
|
|
583
578
|
"genesis_file": genesis_file,
|
|
584
579
|
}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def verify_all_logs() -> dict:
|
|
583
|
+
"""v7.7.15: verify the entire audit chain across all rotated log files.
|
|
584
|
+
|
|
585
|
+
Walks `AUDIT_DIR/audit-*.jsonl` in chronological order, threading
|
|
586
|
+
the chain hash from one file to the next via `start_hash`. Skips
|
|
587
|
+
files from the pre-integrity era (files whose first entry has no
|
|
588
|
+
`_integrity_hash` field, because integrity hashing was introduced
|
|
589
|
+
after some audit logs already existed).
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
A dict with:
|
|
593
|
+
- valid (bool): True if the entire cross-file chain is intact.
|
|
594
|
+
- files_checked (int): Count of integrity-bearing files inspected.
|
|
595
|
+
- files_skipped (int): Count of pre-integrity files skipped.
|
|
596
|
+
- entries_checked (int): Total entries verified across all files.
|
|
597
|
+
- first_tampered_file (str | None): Path to the first file
|
|
598
|
+
whose chain broke, or None if valid.
|
|
599
|
+
- first_tampered_line (int | None): 1-based line number in
|
|
600
|
+
that file where the chain broke, or None if valid.
|
|
601
|
+
- genesis_file (str | None): Path to the first integrity-bearing
|
|
602
|
+
log file (the chain's genesis on this machine), or None if
|
|
603
|
+
no integrity-bearing files exist.
|
|
604
|
+
"""
|
|
605
|
+
return verify_all_logs_in_dir(AUDIT_DIR)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def compute_chain_tip_in_dir(audit_dir) -> dict:
|
|
609
|
+
"""Return the current tip (last hash) of the Python audit chain in
|
|
610
|
+
an explicit directory, plus a verification verdict for that chain.
|
|
611
|
+
|
|
612
|
+
Used by the unified cross-chain verifier to anchor the Python chain
|
|
613
|
+
state into the JS (``src/audit/log.js``) tamper-evident chain via a
|
|
614
|
+
cross-link record, and to reconcile a previously recorded anchor
|
|
615
|
+
against the live chain.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
audit_dir: Path (str or pathlib.Path) to the Python audit dir.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
A dict with:
|
|
622
|
+
- genesis (str): The genesis hash for this chain ("0"*64).
|
|
623
|
+
- tip_hash (str): The last integrity hash, or the genesis hash
|
|
624
|
+
if the chain is empty.
|
|
625
|
+
- entries (int): Total integrity-bearing entries in the chain.
|
|
626
|
+
- valid (bool): Whether the chain verifies end-to-end.
|
|
627
|
+
- chain_id (str): Stable identifier for this chain family.
|
|
628
|
+
"""
|
|
629
|
+
result = verify_all_logs_in_dir(audit_dir)
|
|
630
|
+
audit_dir = Path(audit_dir)
|
|
631
|
+
genesis = "0" * 64
|
|
632
|
+
tip_hash = genesis
|
|
633
|
+
if audit_dir.exists():
|
|
634
|
+
files = sorted(audit_dir.glob("audit-*.jsonl"), key=lambda p: p.stat().st_mtime)
|
|
635
|
+
prev = genesis
|
|
636
|
+
for log_file in files:
|
|
637
|
+
if not _file_has_integrity(str(log_file)):
|
|
638
|
+
continue
|
|
639
|
+
r = verify_log_integrity(str(log_file), start_hash=prev)
|
|
640
|
+
prev = r.get("last_hash", prev)
|
|
641
|
+
tip_hash = prev
|
|
642
|
+
return {
|
|
643
|
+
"genesis": genesis,
|
|
644
|
+
"tip_hash": tip_hash,
|
|
645
|
+
"entries": result.get("entries_checked", 0),
|
|
646
|
+
"valid": bool(result.get("valid", False)),
|
|
647
|
+
"chain_id": "loki-dashboard-audit",
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def compute_prefix_hash_in_dir(audit_dir, n_entries: int) -> dict:
|
|
652
|
+
"""Recompute the dashboard chain hash after exactly the first
|
|
653
|
+
``n_entries`` integrity-bearing entries, walking files in mtime
|
|
654
|
+
order across rotations.
|
|
655
|
+
|
|
656
|
+
This is what lets the unified cross-chain verifier distinguish
|
|
657
|
+
legitimate append-only GROWTH from TAMPER. A cross-link anchor pins
|
|
658
|
+
``(tip_hash, entries)`` at link time; later the live chain may have
|
|
659
|
+
grown. Reconciliation recomputes the hash of the first ``n_entries``
|
|
660
|
+
(the anchored prefix) and checks it still reproduces the anchored
|
|
661
|
+
``tip_hash``. Growth keeps the prefix intact (reproducible); any
|
|
662
|
+
mutation at-or-before the anchor, or truncation below it, makes the
|
|
663
|
+
prefix unreproducible.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
audit_dir: Path (str or pathlib.Path) to the Python audit dir.
|
|
667
|
+
n_entries: Number of leading integrity-bearing entries to hash.
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
A dict with:
|
|
671
|
+
- found (bool): True if at least ``n_entries`` entries exist
|
|
672
|
+
and the prefix was hashed without a chain break inside it.
|
|
673
|
+
- prefix_hash (str): The chain hash after ``n_entries`` entries
|
|
674
|
+
(or the running hash reached if fewer entries exist / a break
|
|
675
|
+
occurred -- in which case ``found`` is False).
|
|
676
|
+
- entries_available (int): Total integrity-bearing entries seen.
|
|
677
|
+
"""
|
|
678
|
+
audit_dir = Path(audit_dir)
|
|
679
|
+
genesis = "0" * 64
|
|
680
|
+
if n_entries <= 0:
|
|
681
|
+
return {"found": True, "prefix_hash": genesis, "entries_available": 0}
|
|
682
|
+
if not audit_dir.exists():
|
|
683
|
+
return {"found": False, "prefix_hash": genesis, "entries_available": 0}
|
|
684
|
+
files = sorted(audit_dir.glob("audit-*.jsonl"), key=lambda p: p.stat().st_mtime)
|
|
685
|
+
prev_hash = genesis
|
|
686
|
+
seen = 0
|
|
687
|
+
started = False
|
|
688
|
+
for log_file in files:
|
|
689
|
+
if not started and not _file_has_integrity(str(log_file)):
|
|
690
|
+
continue
|
|
691
|
+
started = True
|
|
692
|
+
try:
|
|
693
|
+
with open(log_file, "r") as f:
|
|
694
|
+
for line in f:
|
|
695
|
+
line = line.strip()
|
|
696
|
+
if not line:
|
|
697
|
+
continue
|
|
698
|
+
try:
|
|
699
|
+
entry = json.loads(line)
|
|
700
|
+
except json.JSONDecodeError:
|
|
701
|
+
return {"found": False, "prefix_hash": prev_hash,
|
|
702
|
+
"entries_available": seen}
|
|
703
|
+
stored_hash = entry.pop("_integrity_hash", None)
|
|
704
|
+
if stored_hash is None:
|
|
705
|
+
return {"found": False, "prefix_hash": prev_hash,
|
|
706
|
+
"entries_available": seen}
|
|
707
|
+
entry_json = json.dumps(entry, sort_keys=True, default=str)
|
|
708
|
+
expected = _compute_chain_hash(entry_json, prev_hash)
|
|
709
|
+
if stored_hash != expected:
|
|
710
|
+
# Chain broke inside the prefix -> not reproducible.
|
|
711
|
+
return {"found": False, "prefix_hash": prev_hash,
|
|
712
|
+
"entries_available": seen}
|
|
713
|
+
prev_hash = stored_hash
|
|
714
|
+
seen += 1
|
|
715
|
+
if seen == n_entries:
|
|
716
|
+
return {"found": True, "prefix_hash": prev_hash,
|
|
717
|
+
"entries_available": seen}
|
|
718
|
+
except OSError:
|
|
719
|
+
return {"found": False, "prefix_hash": prev_hash,
|
|
720
|
+
"entries_available": seen}
|
|
721
|
+
# Fewer than n_entries entries exist (truncation below the anchor).
|
|
722
|
+
return {"found": False, "prefix_hash": prev_hash, "entries_available": seen}
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _unified_cli() -> int:
|
|
726
|
+
"""Tiny CLI shim so the Node-side unified verifier (or an operator)
|
|
727
|
+
can fetch the Python chain tip / verdict for a given directory as
|
|
728
|
+
JSON. Invoked as:
|
|
729
|
+
|
|
730
|
+
python3 dashboard/audit.py tip <audit_dir>
|
|
731
|
+
python3 dashboard/audit.py verify <audit_dir>
|
|
732
|
+
|
|
733
|
+
Prints a single JSON object to stdout. Returns process exit code 0
|
|
734
|
+
on a valid chain, 1 on an invalid chain, 2 on usage error.
|
|
735
|
+
"""
|
|
736
|
+
argv = sys.argv[1:]
|
|
737
|
+
if len(argv) < 2 or argv[0] not in ("tip", "verify", "prefix"):
|
|
738
|
+
print(json.dumps(
|
|
739
|
+
{"error": "usage: audit.py {tip|verify} <audit_dir> "
|
|
740
|
+
"| prefix <audit_dir> <n_entries>"}))
|
|
741
|
+
return 2
|
|
742
|
+
cmd, audit_dir = argv[0], argv[1]
|
|
743
|
+
if cmd == "tip":
|
|
744
|
+
out = compute_chain_tip_in_dir(audit_dir)
|
|
745
|
+
print(json.dumps(out))
|
|
746
|
+
return 0 if out.get("valid", False) else 1
|
|
747
|
+
if cmd == "prefix":
|
|
748
|
+
if len(argv) < 3:
|
|
749
|
+
print(json.dumps({"error": "prefix requires <n_entries>"}))
|
|
750
|
+
return 2
|
|
751
|
+
try:
|
|
752
|
+
n = int(argv[2])
|
|
753
|
+
except ValueError:
|
|
754
|
+
print(json.dumps({"error": "n_entries must be an integer"}))
|
|
755
|
+
return 2
|
|
756
|
+
out = compute_prefix_hash_in_dir(audit_dir, n)
|
|
757
|
+
print(json.dumps(out))
|
|
758
|
+
return 0 if out.get("found", False) else 1
|
|
759
|
+
out = verify_all_logs_in_dir(audit_dir)
|
|
760
|
+
print(json.dumps(out))
|
|
761
|
+
return 0 if out.get("valid", False) else 1
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
if __name__ == "__main__":
|
|
765
|
+
sys.exit(_unified_cli())
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.50.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -396,7 +396,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
396
396
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
397
397
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
398
398
|
-v $(pwd):/workspace -w /workspace \
|
|
399
|
-
asklokesh/loki-mode:7.
|
|
399
|
+
asklokesh/loki-mode:7.50.0 start ./my-spec.md
|
|
400
400
|
```
|
|
401
401
|
|
|
402
402
|
##### docker compose + .env (no host install)
|
package/docs/siem-integration.md
CHANGED
|
@@ -72,6 +72,67 @@ loki syslog test --message "Test event from Loki Mode"
|
|
|
72
72
|
tail -f /var/log/loki-mode.log
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
## Event Export Module (CEF + Splunk HEC)
|
|
76
|
+
|
|
77
|
+
In addition to syslog forwarding, Loki Mode ships a programmatic exporter for
|
|
78
|
+
audit/security events at `src/observability/siem-export.js`. It provides two
|
|
79
|
+
well-specified, vendor-agnostic formats and an SSRF-safe HEC sender.
|
|
80
|
+
|
|
81
|
+
### Zero egress unless configured
|
|
82
|
+
|
|
83
|
+
The module follows the same gate as the OTEL bridge: nothing leaves the host
|
|
84
|
+
unless an endpoint env var is set. `createHECSenderFromEnv()` returns `null`
|
|
85
|
+
when `LOKI_SPLUNK_HEC_URL` is unset, so there is no code path to the network.
|
|
86
|
+
Endpoint URLs are validated to be `http:`/`https:` only (the same SSRF guard
|
|
87
|
+
the OTLP exporter uses), so a stray `file://` or `gopher://` endpoint is
|
|
88
|
+
rejected before any request is built.
|
|
89
|
+
|
|
90
|
+
### Auto-detected environment variables
|
|
91
|
+
|
|
92
|
+
| Variable | Required | Description |
|
|
93
|
+
|----------|----------|-------------|
|
|
94
|
+
| `LOKI_SPLUNK_HEC_URL` | enables HEC | Splunk HEC collector URL. Presence enables the sender. |
|
|
95
|
+
| `LOKI_SPLUNK_HEC_TOKEN` | no | HEC auth token (sent as `Authorization: Splunk <token>`). |
|
|
96
|
+
| `LOKI_SPLUNK_HEC_INDEX` | no | Target Splunk index. |
|
|
97
|
+
| `LOKI_SPLUNK_HEC_SOURCETYPE` | no | Sourcetype (default `loki:audit`). |
|
|
98
|
+
| `LOKI_CEF_VENDOR` / `LOKI_CEF_PRODUCT` | no | Override CEF vendor/product header fields. |
|
|
99
|
+
|
|
100
|
+
### CEF (Common Event Format)
|
|
101
|
+
|
|
102
|
+
`toCEF(entry)` converts a Loki audit entry into a single-line CEF record.
|
|
103
|
+
Header pipes/backslashes and extension `=`/newlines are escaped per the CEF
|
|
104
|
+
spec, so events cannot break the record framing. Failed events
|
|
105
|
+
(`success: false`) are elevated to severity 8.
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
CEF:0|Autonomi|Loki Mode|7.49.0|revoke_token|revoke_token|8|rt=2026-02-15T14:30:00.000Z suser=alice src=10.0.0.4 cs1=token cs1Label=resourceType outcome=failure msg=expired credential loki.provider=claude loki.cost=4.25
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Nested `details` are flattened under the `loki.` namespace. Standard CEF keys
|
|
112
|
+
are used where they exist (`rt`, `suser`, `src`, `outcome`, `msg`, `cs1..cs3`).
|
|
113
|
+
|
|
114
|
+
### Splunk HEC JSON
|
|
115
|
+
|
|
116
|
+
`toHEC(entry)` wraps an audit entry in a Splunk HEC envelope (epoch-seconds
|
|
117
|
+
`time`, `sourcetype`, optional `index`, and the raw `event`). `HECSender.send()`
|
|
118
|
+
POSTs it fire-and-forget; network errors are logged, never thrown, so
|
|
119
|
+
observability never breaks the run.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
export LOKI_SPLUNK_HEC_URL=https://splunk.example.com:8088/services/collector
|
|
123
|
+
export LOKI_SPLUNK_HEC_TOKEN=your-hec-token
|
|
124
|
+
export LOKI_SPLUNK_HEC_INDEX=security
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### GitHub Enterprise SAML SSO (docs-only follow-up)
|
|
128
|
+
|
|
129
|
+
Ingesting GitHub Enterprise SAML/SSO sign-in events is a documented follow-up,
|
|
130
|
+
not a shipped code path. Those events live in the GitHub audit log API and
|
|
131
|
+
require an org-scoped admin token plus an outbound polling client, which is out
|
|
132
|
+
of scope for the local audit path. Recommended approach today: pull the GitHub
|
|
133
|
+
Enterprise audit log via its native streaming to your SIEM (Splunk/Datadog/
|
|
134
|
+
Azure Event Hubs) and correlate on `actor`/`user_id` with Loki Mode events.
|
|
135
|
+
|
|
75
136
|
## Splunk Integration
|
|
76
137
|
|
|
77
138
|
### Method 1: Splunk Universal Forwarder
|
|
@@ -262,6 +323,47 @@ export LOKI_SYSLOG_FORMAT=cef
|
|
|
262
323
|
# rt=2026-02-15T14:30:00Z suser=user cs1=claude cs1Label=Provider
|
|
263
324
|
```
|
|
264
325
|
|
|
326
|
+
## OTEL Vendor Templates (Datadog, Honeycomb)
|
|
327
|
+
|
|
328
|
+
Loki Mode already emits OpenTelemetry traces/metrics when `LOKI_OTEL_ENDPOINT`
|
|
329
|
+
is set (see `src/observability/otel.js`). Ready-to-use vendor templates live in
|
|
330
|
+
`src/observability/siem-export.js` (`OTEL_TEMPLATES`) and produce the exact set
|
|
331
|
+
of env vars to ship to a vendor. They are recipes, not egress: copy the output
|
|
332
|
+
into your shell.
|
|
333
|
+
|
|
334
|
+
### Datadog
|
|
335
|
+
|
|
336
|
+
Datadog ingests OTLP/HTTP via the Datadog Agent's OTLP receiver (default
|
|
337
|
+
`:4318`) or, agentless, via the OpenTelemetry Collector contrib exporter.
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
# Local Datadog Agent OTLP receiver (recommended)
|
|
341
|
+
export LOKI_OTEL_ENDPOINT=http://localhost:4318
|
|
342
|
+
export LOKI_SERVICE_NAME=loki-mode
|
|
343
|
+
|
|
344
|
+
# Agentless intake (set API key + site)
|
|
345
|
+
export LOKI_OTEL_ENDPOINT=http://localhost:4318
|
|
346
|
+
export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
|
|
347
|
+
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,dd.site=datadoghq.com"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`site` examples: `datadoghq.com`, `datadoghq.eu`, `us5.datadoghq.com`.
|
|
351
|
+
|
|
352
|
+
### Honeycomb
|
|
353
|
+
|
|
354
|
+
Honeycomb ingests OTLP/HTTP directly. Auth is the `x-honeycomb-team` header.
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
export LOKI_OTEL_ENDPOINT=https://api.honeycomb.io # or https://api.eu1.honeycomb.io
|
|
358
|
+
export LOKI_SERVICE_NAME=loki-mode
|
|
359
|
+
export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=YOUR_API_KEY,x-honeycomb-dataset=loki"
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Note: `OTEL_EXPORTER_OTLP_HEADERS` is honored when the real `@opentelemetry`
|
|
363
|
+
SDK is installed (otel.js prefers it and falls back to the built-in JSON
|
|
364
|
+
exporter). For Datadog the local-agent path holds the API key, so the header is
|
|
365
|
+
optional there.
|
|
366
|
+
|
|
265
367
|
## Datadog Security Monitoring
|
|
266
368
|
|
|
267
369
|
### Log Collection
|