@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
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/CHANGELOG.md +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/tools/agent-forensics.sh +186 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/agents.py +183 -0
- package/ui/api/__init__.py +84 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/api/submit.py +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
json_escape() {
|
|
5
|
+
# Escape backslashes first, then quotes, then control chars for valid JSON
|
|
6
|
+
# Finding 5: Handle control characters (\n, \r, \t, C0)
|
|
7
|
+
local s="$1"
|
|
8
|
+
s="${s//\\/\\\\}"
|
|
9
|
+
s="${s//\"/\\\"}"
|
|
10
|
+
s="${s//$'\n'/\\n}"
|
|
11
|
+
s="${s//$'\r'/\\r}"
|
|
12
|
+
s="${s//$'\t'/\\t}"
|
|
13
|
+
printf '%s' "$s"
|
|
14
|
+
}
|
|
15
|
+
compute_sha256() {
|
|
16
|
+
# P1-Bug2 fix: Single helper for sha256sum with fallback to shasum
|
|
17
|
+
# Reads from stdin, outputs hex hash
|
|
18
|
+
local hash_bin
|
|
19
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
20
|
+
hash_bin="sha256sum"
|
|
21
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
22
|
+
hash_bin="shasum -a 256"
|
|
23
|
+
else
|
|
24
|
+
printf 'ERROR: sha256sum or shasum not found in PATH
|
|
25
|
+
' >&2
|
|
26
|
+
return 1
|
|
27
|
+
fi
|
|
28
|
+
$hash_bin | awk '{print $1}'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
acquire_audit_lock() {
|
|
33
|
+
# Finding 1: Mkdir-based atomic lock for audit log write safety
|
|
34
|
+
local lock_dir="$1"
|
|
35
|
+
local timeout=300
|
|
36
|
+
local start_time
|
|
37
|
+
start_time=$(date +%s)
|
|
38
|
+
|
|
39
|
+
while true; do
|
|
40
|
+
if mkdir "$lock_dir" 2>/dev/null; then
|
|
41
|
+
# Acquired lock successfully
|
|
42
|
+
echo "$$" > "$lock_dir/pid"
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Check if lock is stale (>timeout seconds old)
|
|
47
|
+
if [ -f "$lock_dir/pid" ]; then
|
|
48
|
+
local lock_time
|
|
49
|
+
lock_time=$(stat -c %Y "$lock_dir" 2>/dev/null || stat -f %m "$lock_dir" 2>/dev/null || echo 0)
|
|
50
|
+
local current_time
|
|
51
|
+
current_time=$(date +%s)
|
|
52
|
+
if [ $((current_time - lock_time)) -gt $timeout ]; then
|
|
53
|
+
# Stale lock; force reclaim atomically
|
|
54
|
+
rm -rf "$lock_dir" 2>/dev/null
|
|
55
|
+
mkdir "$lock_dir" 2>/dev/null && echo "$$" > "$lock_dir/pid" && return 0
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Check timeout
|
|
60
|
+
if [ $(($(date +%s) - start_time)) -gt 10 ]; then
|
|
61
|
+
# Lock holder is stuck; give up after 10s
|
|
62
|
+
return 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
sleep 0.1
|
|
66
|
+
done
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
release_audit_lock() {
|
|
70
|
+
# P0 fix: Release the lock directory only if we own it (pid matches)
|
|
71
|
+
local lock_dir="$1"
|
|
72
|
+
if [ -f "$lock_dir/pid" ]; then
|
|
73
|
+
local lock_pid
|
|
74
|
+
lock_pid=$(cat "$lock_dir/pid" 2>/dev/null || echo "")
|
|
75
|
+
if [ "$lock_pid" = "$$" ]; then
|
|
76
|
+
rm -rf "$lock_dir" 2>/dev/null
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get_previous_hash() {
|
|
82
|
+
# Finding 6: Fallback for missing sha256sum, fail loudly if unavailable
|
|
83
|
+
local audit_log="$1"
|
|
84
|
+
if [ ! -f "$audit_log" ] || [ ! -s "$audit_log" ]; then
|
|
85
|
+
printf 'GENESIS'
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
local hash_bin
|
|
90
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
91
|
+
hash_bin="sha256sum"
|
|
92
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
93
|
+
hash_bin="shasum -a 256"
|
|
94
|
+
else
|
|
95
|
+
printf 'ERROR: sha256sum or shasum not found in PATH\n' >&2
|
|
96
|
+
return 1
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
tail -n 1 "$audit_log" | tr -d '\n' | $hash_bin | awk '{print $1}'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get_next_seq() {
|
|
103
|
+
# Finding 2: Get monotonically increasing sequence number
|
|
104
|
+
local audit_log="$1"
|
|
105
|
+
if [ ! -f "$audit_log" ] || [ ! -s "$audit_log" ]; then
|
|
106
|
+
echo 1
|
|
107
|
+
return 0
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
local last_seq
|
|
111
|
+
last_seq=$(tail -n 1 "$audit_log" | python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('seq', 0))" 2>/dev/null || echo 0)
|
|
112
|
+
echo $((last_seq + 1))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
verify_audit_log() {
|
|
116
|
+
# P1-Bug1 fix: Acquire write lock while reading/verifying sidecar
|
|
117
|
+
# Prevents false truncation positive during concurrent appends
|
|
118
|
+
# Finding 2: Include truncation detection via tail hash sidecar and seq field
|
|
119
|
+
local audit_log="$1"
|
|
120
|
+
if [ ! -f "$audit_log" ]; then
|
|
121
|
+
printf 'Error: Audit log not found at %s
|
|
122
|
+
' "$audit_log" >&2
|
|
123
|
+
return 1
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if [ ! -s "$audit_log" ]; then
|
|
127
|
+
printf 'Audit log is empty or does not exist
|
|
128
|
+
'
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
local state_dir
|
|
133
|
+
state_dir=$(dirname "$audit_log")
|
|
134
|
+
local lock_dir="$state_dir/.audit-log-lock"
|
|
135
|
+
local tail_hash_file="$state_dir/.audit-tail-hash"
|
|
136
|
+
|
|
137
|
+
# P1-Bug1: Acquire lock before reading sidecar to prevent race
|
|
138
|
+
if ! acquire_audit_lock "$lock_dir"; then
|
|
139
|
+
printf 'Warning: Could not acquire lock for verification; skipping sidecar check
|
|
140
|
+
' >&2
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
local line_num=0
|
|
144
|
+
local prev_line=""
|
|
145
|
+
local expected_hash=""
|
|
146
|
+
local actual_prev_hash=""
|
|
147
|
+
local prev_seq=0
|
|
148
|
+
|
|
149
|
+
while IFS= read -r line; do
|
|
150
|
+
line_num=$((line_num + 1))
|
|
151
|
+
|
|
152
|
+
if [ $line_num -eq 1 ]; then
|
|
153
|
+
expected_hash="GENESIS"
|
|
154
|
+
else
|
|
155
|
+
expected_hash=$(printf '%s' "$prev_line" | compute_sha256)
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
actual_prev_hash=$(printf '%s' "$line" | python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('prev_hash', 'MISSING'))" 2>/dev/null)
|
|
159
|
+
|
|
160
|
+
if [ "$actual_prev_hash" = "MISSING" ]; then
|
|
161
|
+
release_audit_lock "$lock_dir"
|
|
162
|
+
printf 'Error: Line %d missing prev_hash field
|
|
163
|
+
' "$line_num" >&2
|
|
164
|
+
return 1
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
if [ "$actual_prev_hash" != "$expected_hash" ]; then
|
|
168
|
+
release_audit_lock "$lock_dir"
|
|
169
|
+
printf 'Error: Hash chain broken at line %d
|
|
170
|
+
' "$line_num" >&2
|
|
171
|
+
printf ' Expected prev_hash: %s
|
|
172
|
+
' "$expected_hash" >&2
|
|
173
|
+
printf ' Actual prev_hash: %s
|
|
174
|
+
' "$actual_prev_hash" >&2
|
|
175
|
+
return 1
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
# Check seq monotonicity
|
|
179
|
+
local current_seq
|
|
180
|
+
current_seq=$(printf '%s' "$line" | python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('seq', 0))" 2>/dev/null || echo 0)
|
|
181
|
+
if [ "$current_seq" -le "$prev_seq" ] && [ $line_num -gt 1 ]; then
|
|
182
|
+
release_audit_lock "$lock_dir"
|
|
183
|
+
printf 'Error: Sequence number not monotonic at line %d (prev: %d, current: %d)
|
|
184
|
+
' "$line_num" "$prev_seq" "$current_seq" >&2
|
|
185
|
+
return 1
|
|
186
|
+
fi
|
|
187
|
+
prev_seq=$current_seq
|
|
188
|
+
|
|
189
|
+
prev_line="$line"
|
|
190
|
+
done < "$audit_log"
|
|
191
|
+
|
|
192
|
+
# Check truncation via tail hash anchor (within lock)
|
|
193
|
+
if [ -f "$tail_hash_file" ]; then
|
|
194
|
+
local stored_tail_hash
|
|
195
|
+
stored_tail_hash=$(head -n 1 "$tail_hash_file" 2>/dev/null)
|
|
196
|
+
local actual_tail_hash
|
|
197
|
+
actual_tail_hash=$(tail -n 1 "$audit_log" | tr -d '
|
|
198
|
+
' | compute_sha256)
|
|
199
|
+
|
|
200
|
+
if [ "$stored_tail_hash" != "$actual_tail_hash" ]; then
|
|
201
|
+
release_audit_lock "$lock_dir"
|
|
202
|
+
printf 'TRUNCATION SUSPECTED: Tail hash mismatch (stored: %s, actual: %s)
|
|
203
|
+
' "$stored_tail_hash" "$actual_tail_hash" >&2
|
|
204
|
+
return 1
|
|
205
|
+
fi
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
release_audit_lock "$lock_dir"
|
|
209
|
+
|
|
210
|
+
if [ $line_num -gt 0 ]; then
|
|
211
|
+
printf 'Audit log verification OK (%d entries)
|
|
212
|
+
' "$line_num"
|
|
213
|
+
fi
|
|
214
|
+
return 0
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
check_branch_policy() {
|
|
218
|
+
# Parse git pre-push stdin to check if any remote-ref targets main or master
|
|
219
|
+
# Format: <local-ref> <local-sha> <remote-ref> <remote-sha>
|
|
220
|
+
# This catches attempts like: git push origin HEAD:main (even from feature branch)
|
|
221
|
+
# Finding 3: Handle tty mode and final line without trailing newline
|
|
222
|
+
if [ -t 0 ]; then
|
|
223
|
+
# Running interactively on a tty; skip stdin processing with note
|
|
224
|
+
# but still check current branch as fallback
|
|
225
|
+
:
|
|
226
|
+
else
|
|
227
|
+
# Not a tty; read stdin normally
|
|
228
|
+
while IFS=' ' read -r local_ref local_sha remote_ref remote_sha || [ -n "$local_ref" ]; do
|
|
229
|
+
# Skip empty lines
|
|
230
|
+
if [ -z "$remote_ref" ]; then
|
|
231
|
+
continue
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
# Block if attempting to push to main or master
|
|
235
|
+
if [ "$remote_ref" = "refs/heads/main" ] || [ "$remote_ref" = "refs/heads/master" ]; then
|
|
236
|
+
return 1
|
|
237
|
+
fi
|
|
238
|
+
done
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
# If no protected branch in stdin, also check current branch as fallback
|
|
242
|
+
# (for safety, in case stdin is empty or hook runs without git pre-push)
|
|
243
|
+
local current_branch
|
|
244
|
+
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
245
|
+
|
|
246
|
+
if [ "$current_branch" = "main" ] || [ "$current_branch" = "master" ]; then
|
|
247
|
+
return 1
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
return 0
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
check_secret_scan() {
|
|
254
|
+
local scan_bin
|
|
255
|
+
if command -v python >/dev/null 2>&1; then
|
|
256
|
+
scan_bin="python"
|
|
257
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
258
|
+
scan_bin="python3"
|
|
259
|
+
else
|
|
260
|
+
scan_bin="python"
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
local aesop_root="${AESOP_ROOT:-$HOME/aesop}"
|
|
264
|
+
local scan_script="$aesop_root/tools/secret_scan.py"
|
|
265
|
+
|
|
266
|
+
if [ ! -f "$scan_script" ]; then
|
|
267
|
+
# Scanner not found: log event and warn to stderr, but don't block (fail-open)
|
|
268
|
+
log_event "secret_scan_unavailable"
|
|
269
|
+
printf 'Warning: secret_scan.py not found at %s\n' "$scan_script" >&2
|
|
270
|
+
return 0
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
# Run scanner and capture output; surface ALLOWED-DOC lines to stderr for visibility
|
|
274
|
+
local scan_output
|
|
275
|
+
scan_output=$("$scan_bin" "$scan_script" --staged 2>&1)
|
|
276
|
+
local exit_code=$?
|
|
277
|
+
|
|
278
|
+
# Surface all output including ALLOWED-DOC findings to stderr
|
|
279
|
+
if [ -n "$scan_output" ]; then
|
|
280
|
+
printf '%s\n' "$scan_output" >&2
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
return $exit_code
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
log_event() {
|
|
287
|
+
# Finding 1 & 2: Acquire lock before read-modify-append, add seq field, update sidecar
|
|
288
|
+
local event_type="$1"
|
|
289
|
+
local aesop_root="${AESOP_ROOT:-$HOME/aesop}"
|
|
290
|
+
local state_dir="$aesop_root/state"
|
|
291
|
+
local audit_log="$state_dir/SECURITY-AUDIT.log"
|
|
292
|
+
local lock_dir="$state_dir/.audit-log-lock"
|
|
293
|
+
local ts
|
|
294
|
+
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
295
|
+
local repo_name
|
|
296
|
+
repo_name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || echo 'unknown')")
|
|
297
|
+
local user
|
|
298
|
+
user=$(git config user.name 2>/dev/null || echo "unknown")
|
|
299
|
+
|
|
300
|
+
mkdir -p "$state_dir" 2>/dev/null
|
|
301
|
+
|
|
302
|
+
# Acquire write lock
|
|
303
|
+
if ! acquire_audit_lock "$lock_dir"; then
|
|
304
|
+
# Log write blocked by lock; fail-open (don't block push)
|
|
305
|
+
return 0
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
local prev_hash
|
|
309
|
+
prev_hash=$(get_previous_hash "$audit_log")
|
|
310
|
+
local seq
|
|
311
|
+
seq=$(get_next_seq "$audit_log")
|
|
312
|
+
|
|
313
|
+
printf '{"seq":%d,"prev_hash":"%s","ts":"%s","repo":"%s","event":"%s","user":"%s"}\n' "$seq" "$prev_hash" "$ts" "$repo_name" "$(json_escape "$event_type")" "$(json_escape "$user")" >> "$audit_log" 2>/dev/null
|
|
314
|
+
|
|
315
|
+
# Update tail hash sidecar
|
|
316
|
+
if [ -s "$audit_log" ]; then
|
|
317
|
+
tail -n 1 "$audit_log" | tr -d '\n' | compute_sha256 > "$state_dir/.audit-tail-hash" 2>/dev/null
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
release_audit_lock "$lock_dir"
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
log_block() {
|
|
324
|
+
# Finding 1 & 2: Acquire lock before read-modify-append, add seq field, update sidecar
|
|
325
|
+
local reason="$1"
|
|
326
|
+
local aesop_root="${AESOP_ROOT:-$HOME/aesop}"
|
|
327
|
+
local state_dir="$aesop_root/state"
|
|
328
|
+
local audit_log="$state_dir/SECURITY-AUDIT.log"
|
|
329
|
+
local lock_dir="$state_dir/.audit-log-lock"
|
|
330
|
+
local ts
|
|
331
|
+
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
332
|
+
local repo_name
|
|
333
|
+
repo_name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || echo 'unknown')")
|
|
334
|
+
local user
|
|
335
|
+
user=$(git config user.name 2>/dev/null || echo "unknown")
|
|
336
|
+
|
|
337
|
+
mkdir -p "$state_dir" 2>/dev/null
|
|
338
|
+
|
|
339
|
+
# Acquire write lock
|
|
340
|
+
if ! acquire_audit_lock "$lock_dir"; then
|
|
341
|
+
# Log write blocked by lock; fail-open (don't block push)
|
|
342
|
+
return 0
|
|
343
|
+
fi
|
|
344
|
+
|
|
345
|
+
local prev_hash
|
|
346
|
+
prev_hash=$(get_previous_hash "$audit_log")
|
|
347
|
+
local seq
|
|
348
|
+
seq=$(get_next_seq "$audit_log")
|
|
349
|
+
|
|
350
|
+
printf '{"seq":%d,"prev_hash":"%s","ts":"%s","repo":"%s","event":"push_blocked","reason":"%s","user":"%s"}\n' "$seq" "$prev_hash" "$ts" "$repo_name" "$(json_escape "$reason")" "$(json_escape "$user")" >> "$audit_log" 2>/dev/null
|
|
351
|
+
|
|
352
|
+
# Update tail hash sidecar
|
|
353
|
+
if [ -s "$audit_log" ]; then
|
|
354
|
+
tail -n 1 "$audit_log" | tr -d '\n' | compute_sha256 > "$state_dir/.audit-tail-hash" 2>/dev/null
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
release_audit_lock "$lock_dir"
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
run_test_mode() {
|
|
361
|
+
local test_passed=0
|
|
362
|
+
local test_failed=0
|
|
363
|
+
local tmpdir
|
|
364
|
+
tmpdir=$(mktemp -d)
|
|
365
|
+
trap "rm -rf '$tmpdir'" EXIT
|
|
366
|
+
|
|
367
|
+
printf '\n=== Test 1: Branch policy check ===\n'
|
|
368
|
+
(
|
|
369
|
+
cd "$tmpdir" || exit 1
|
|
370
|
+
git init -q
|
|
371
|
+
git config user.email "test@example.com"
|
|
372
|
+
git config user.name "Test User"
|
|
373
|
+
echo "dummy" > file.txt
|
|
374
|
+
git add file.txt
|
|
375
|
+
git commit -q -m "initial"
|
|
376
|
+
git checkout -q -b main 2>/dev/null || git branch -M main
|
|
377
|
+
|
|
378
|
+
if check_branch_policy; then
|
|
379
|
+
printf 'FAIL: Should have blocked on main branch\n'
|
|
380
|
+
exit 1
|
|
381
|
+
fi
|
|
382
|
+
printf 'PASS: Correctly blocked on main branch\n'
|
|
383
|
+
)
|
|
384
|
+
if [ $? -eq 0 ]; then
|
|
385
|
+
test_passed=$((test_passed + 1))
|
|
386
|
+
else
|
|
387
|
+
test_failed=$((test_failed + 1))
|
|
388
|
+
fi
|
|
389
|
+
|
|
390
|
+
printf '\n=== Test 2: Branch policy allows feature branches ===\n'
|
|
391
|
+
(
|
|
392
|
+
cd "$tmpdir" || exit 1
|
|
393
|
+
git checkout -q -b feature/test 2>/dev/null
|
|
394
|
+
|
|
395
|
+
if check_branch_policy; then
|
|
396
|
+
printf 'PASS: Correctly allowed feature branch\n'
|
|
397
|
+
else
|
|
398
|
+
printf 'FAIL: Should have allowed feature branch\n'
|
|
399
|
+
exit 1
|
|
400
|
+
fi
|
|
401
|
+
)
|
|
402
|
+
if [ $? -eq 0 ]; then
|
|
403
|
+
test_passed=$((test_passed + 1))
|
|
404
|
+
else
|
|
405
|
+
test_failed=$((test_failed + 1))
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
printf '\n=== Test 3: Audit log format ===\n'
|
|
409
|
+
(
|
|
410
|
+
export AESOP_ROOT="$tmpdir/aesop"
|
|
411
|
+
mkdir -p "$AESOP_ROOT/state"
|
|
412
|
+
log_block "test_reason"
|
|
413
|
+
|
|
414
|
+
if [ ! -f "$AESOP_ROOT/state/SECURITY-AUDIT.log" ]; then
|
|
415
|
+
printf 'FAIL: Audit log not created\n'
|
|
416
|
+
exit 1
|
|
417
|
+
fi
|
|
418
|
+
|
|
419
|
+
audit_line=$(tail -n 1 "$AESOP_ROOT/state/SECURITY-AUDIT.log")
|
|
420
|
+
if ! printf '%s' "$audit_line" | python3 -m json.tool >/dev/null 2>&1; then
|
|
421
|
+
printf 'FAIL: Audit log entry is not valid JSON\n'
|
|
422
|
+
printf 'Entry: %s\n' "$audit_line"
|
|
423
|
+
exit 1
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
if printf '%s' "$audit_line" | grep -q '"event":"push_blocked"'; then
|
|
427
|
+
printf 'PASS: Audit log entry valid JSON with correct event type\n'
|
|
428
|
+
else
|
|
429
|
+
printf 'FAIL: Audit log entry missing correct event\n'
|
|
430
|
+
exit 1
|
|
431
|
+
fi
|
|
432
|
+
)
|
|
433
|
+
if [ $? -eq 0 ]; then
|
|
434
|
+
test_passed=$((test_passed + 1))
|
|
435
|
+
else
|
|
436
|
+
test_failed=$((test_failed + 1))
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
printf '\n=== Test 4: JSON escaping with special characters ===\n'
|
|
440
|
+
(
|
|
441
|
+
export AESOP_ROOT="$tmpdir/aesop"
|
|
442
|
+
mkdir -p "$AESOP_ROOT/state"
|
|
443
|
+
git config user.name 'John "Jack" Doe'
|
|
444
|
+
log_block "reason_with_backslash\\test"
|
|
445
|
+
|
|
446
|
+
if [ ! -f "$AESOP_ROOT/state/SECURITY-AUDIT.log" ]; then
|
|
447
|
+
printf 'FAIL: Audit log not created for special chars test\n'
|
|
448
|
+
exit 1
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
audit_line=$(tail -n 1 "$AESOP_ROOT/state/SECURITY-AUDIT.log")
|
|
452
|
+
if ! printf '%s' "$audit_line" | python3 -m json.tool >/dev/null 2>&1; then
|
|
453
|
+
printf 'FAIL: JSON with escaped chars is invalid\n'
|
|
454
|
+
printf 'Entry: %s\n' "$audit_line"
|
|
455
|
+
exit 1
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
if printf '%s' "$audit_line" | grep -q 'John.*Jack.*Doe'; then
|
|
459
|
+
printf 'PASS: JSON correctly escapes quotes in user names\n'
|
|
460
|
+
else
|
|
461
|
+
printf 'FAIL: JSON escaping incomplete\n'
|
|
462
|
+
exit 1
|
|
463
|
+
fi
|
|
464
|
+
)
|
|
465
|
+
if [ $? -eq 0 ]; then
|
|
466
|
+
test_passed=$((test_passed + 1))
|
|
467
|
+
else
|
|
468
|
+
test_failed=$((test_failed + 1))
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
printf '\n=== Test 5: stdin handling (git hook compatibility) ===\n'
|
|
472
|
+
(
|
|
473
|
+
cd "$tmpdir" || exit 1
|
|
474
|
+
git checkout -q feature/test 2>/dev/null
|
|
475
|
+
|
|
476
|
+
# Simulate git pre-push stdin with ref info
|
|
477
|
+
local_sha=$(git rev-parse HEAD 2>/dev/null || echo "0000000")
|
|
478
|
+
printf '%s\n' "refs/heads/feature/test $local_sha refs/heads/feature/test 0000000000000000000000000000000000000000" | {
|
|
479
|
+
if check_branch_policy >/dev/null 2>&1; then
|
|
480
|
+
printf 'PASS: Hook accepts stdin without choking\n'
|
|
481
|
+
else
|
|
482
|
+
printf 'FAIL: Hook failed with stdin input\n'
|
|
483
|
+
exit 1
|
|
484
|
+
fi
|
|
485
|
+
}
|
|
486
|
+
) || {
|
|
487
|
+
printf 'FAIL: stdin test exited with error\n'
|
|
488
|
+
test_failed=$((test_failed + 1))
|
|
489
|
+
}
|
|
490
|
+
if [ $? -eq 0 ]; then
|
|
491
|
+
test_passed=$((test_passed + 1))
|
|
492
|
+
fi
|
|
493
|
+
|
|
494
|
+
printf '\n=== Test 6: stdin refspec bypass detection (HEAD:main) ===\n'
|
|
495
|
+
(
|
|
496
|
+
cd "$tmpdir" || exit 1
|
|
497
|
+
git checkout -q feature/test 2>/dev/null || git checkout -q -b feature/bypass_test 2>/dev/null
|
|
498
|
+
|
|
499
|
+
# Simulate git pre-push stdin for: git push origin HEAD:main
|
|
500
|
+
# This is an explicit refspec that pushes to main even though local HEAD is feature/test
|
|
501
|
+
# The fixed check_branch_policy MUST block this by checking remote-ref in stdin
|
|
502
|
+
local_sha=$(git rev-parse HEAD 2>/dev/null || echo "0000000")
|
|
503
|
+
remote_main_sha="0000000000000000000000000000000000000000"
|
|
504
|
+
|
|
505
|
+
# Stdin format: <local-ref> <local-sha> <remote-ref> <remote-sha>
|
|
506
|
+
# For "git push origin HEAD:main": refs/heads/feature/test <sha> refs/heads/main 0000...
|
|
507
|
+
printf '%s\n' "refs/heads/feature/test $local_sha refs/heads/main $remote_main_sha" | {
|
|
508
|
+
if check_branch_policy >/dev/null 2>&1; then
|
|
509
|
+
printf 'FAIL: Should have blocked push to main via stdin refspec\n'
|
|
510
|
+
exit 1
|
|
511
|
+
else
|
|
512
|
+
printf 'PASS: Correctly blocked push to main via stdin refspec\n'
|
|
513
|
+
fi
|
|
514
|
+
}
|
|
515
|
+
)
|
|
516
|
+
if [ $? -eq 0 ]; then
|
|
517
|
+
test_passed=$((test_passed + 1))
|
|
518
|
+
else
|
|
519
|
+
test_failed=$((test_failed + 1))
|
|
520
|
+
fi
|
|
521
|
+
|
|
522
|
+
printf '\n=== Test 7: Secret scan unavailable logs audit event ===\n'
|
|
523
|
+
(
|
|
524
|
+
export AESOP_ROOT="$tmpdir/aesop_no_scanner"
|
|
525
|
+
mkdir -p "$AESOP_ROOT/state"
|
|
526
|
+
|
|
527
|
+
# Capture stderr to verify warning is printed
|
|
528
|
+
stderr_output=$( { check_secret_scan; } 2>&1 1>/dev/null )
|
|
529
|
+
exit_code=$?
|
|
530
|
+
|
|
531
|
+
# Should return 0 (fail-open)
|
|
532
|
+
if [ "$exit_code" -ne 0 ]; then
|
|
533
|
+
printf 'FAIL: check_secret_scan should return 0 when scanner missing (fail-open)\n'
|
|
534
|
+
exit 1
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
# Should have logged a "secret_scan_unavailable" event
|
|
538
|
+
if [ ! -f "$AESOP_ROOT/state/SECURITY-AUDIT.log" ]; then
|
|
539
|
+
printf 'FAIL: Audit log not created when scanner is unavailable\n'
|
|
540
|
+
exit 1
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
audit_line=$(tail -n 1 "$AESOP_ROOT/state/SECURITY-AUDIT.log")
|
|
544
|
+
|
|
545
|
+
# Verify JSON is valid
|
|
546
|
+
if ! printf '%s' "$audit_line" | python3 -m json.tool >/dev/null 2>&1; then
|
|
547
|
+
printf 'FAIL: Audit log entry is not valid JSON\n'
|
|
548
|
+
printf 'Entry: %s\n' "$audit_line"
|
|
549
|
+
exit 1
|
|
550
|
+
fi
|
|
551
|
+
|
|
552
|
+
# Verify event type is "secret_scan_unavailable"
|
|
553
|
+
if ! printf '%s' "$audit_line" | grep -q '"event":"secret_scan_unavailable"'; then
|
|
554
|
+
printf 'FAIL: Audit log entry missing correct event type\n'
|
|
555
|
+
printf 'Expected event: "secret_scan_unavailable"\n'
|
|
556
|
+
printf 'Entry: %s\n' "$audit_line"
|
|
557
|
+
exit 1
|
|
558
|
+
fi
|
|
559
|
+
|
|
560
|
+
# Verify a warning was printed to stderr
|
|
561
|
+
if ! printf '%s' "$stderr_output" | grep -q -i 'unavailable\|missing\|not found'; then
|
|
562
|
+
printf 'FAIL: No warning message printed to stderr\n'
|
|
563
|
+
printf 'stderr was: %s\n' "$stderr_output"
|
|
564
|
+
exit 1
|
|
565
|
+
fi
|
|
566
|
+
|
|
567
|
+
printf 'PASS: Secret scan unavailable logged with event and stderr warning\n'
|
|
568
|
+
)
|
|
569
|
+
if [ $? -eq 0 ]; then
|
|
570
|
+
test_passed=$((test_passed + 1))
|
|
571
|
+
else
|
|
572
|
+
test_failed=$((test_failed + 1))
|
|
573
|
+
fi
|
|
574
|
+
|
|
575
|
+
printf '\n=== Test 8: Hash-chain audit log (GENESIS first event) ===\n'
|
|
576
|
+
(
|
|
577
|
+
export AESOP_ROOT="$tmpdir/aesop_hashchain"
|
|
578
|
+
mkdir -p "$AESOP_ROOT/state"
|
|
579
|
+
log_block "test_block_1"
|
|
580
|
+
|
|
581
|
+
if [ ! -f "$AESOP_ROOT/state/SECURITY-AUDIT.log" ]; then
|
|
582
|
+
printf 'FAIL: Audit log not created\n'
|
|
583
|
+
exit 1
|
|
584
|
+
fi
|
|
585
|
+
|
|
586
|
+
audit_line=$(tail -n 1 "$AESOP_ROOT/state/SECURITY-AUDIT.log")
|
|
587
|
+
if ! printf '%s' "$audit_line" | grep -q '"prev_hash":"GENESIS"'; then
|
|
588
|
+
printf 'FAIL: First event should have prev_hash=GENESIS\n'
|
|
589
|
+
printf 'Entry: %s\n' "$audit_line"
|
|
590
|
+
exit 1
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
if ! printf '%s' "$audit_line" | python3 -m json.tool >/dev/null 2>&1; then
|
|
594
|
+
printf 'FAIL: Hash-chained entry is not valid JSON\n'
|
|
595
|
+
printf 'Entry: %s\n' "$audit_line"
|
|
596
|
+
exit 1
|
|
597
|
+
fi
|
|
598
|
+
|
|
599
|
+
printf 'PASS: First event has GENESIS prev_hash and valid JSON\n'
|
|
600
|
+
)
|
|
601
|
+
if [ $? -eq 0 ]; then
|
|
602
|
+
test_passed=$((test_passed + 1))
|
|
603
|
+
else
|
|
604
|
+
test_failed=$((test_failed + 1))
|
|
605
|
+
fi
|
|
606
|
+
|
|
607
|
+
printf '\n=== Test 9: Hash-chain builds across 2+ events ===\n'
|
|
608
|
+
(
|
|
609
|
+
export AESOP_ROOT="$tmpdir/aesop_hashchain2"
|
|
610
|
+
mkdir -p "$AESOP_ROOT/state"
|
|
611
|
+
log_block "test_block_1"
|
|
612
|
+
log_block "test_block_2"
|
|
613
|
+
|
|
614
|
+
if [ ! -f "$AESOP_ROOT/state/SECURITY-AUDIT.log" ]; then
|
|
615
|
+
printf 'FAIL: Audit log not created\n'
|
|
616
|
+
exit 1
|
|
617
|
+
fi
|
|
618
|
+
|
|
619
|
+
line_count=$(wc -l < "$AESOP_ROOT/state/SECURITY-AUDIT.log")
|
|
620
|
+
if [ "$line_count" -ne 2 ]; then
|
|
621
|
+
printf 'FAIL: Expected 2 audit log entries, got %d\n' "$line_count"
|
|
622
|
+
exit 1
|
|
623
|
+
fi
|
|
624
|
+
|
|
625
|
+
line1=$(head -n 1 "$AESOP_ROOT/state/SECURITY-AUDIT.log")
|
|
626
|
+
line2=$(tail -n 1 "$AESOP_ROOT/state/SECURITY-AUDIT.log")
|
|
627
|
+
|
|
628
|
+
if ! printf '%s' "$line1" | grep -q '"prev_hash":"GENESIS"'; then
|
|
629
|
+
printf 'FAIL: First event should have prev_hash=GENESIS\n'
|
|
630
|
+
exit 1
|
|
631
|
+
fi
|
|
632
|
+
|
|
633
|
+
if ! printf '%s' "$line2" | grep -q '"prev_hash":'; then
|
|
634
|
+
printf 'FAIL: Second event should have prev_hash field\n'
|
|
635
|
+
exit 1
|
|
636
|
+
fi
|
|
637
|
+
|
|
638
|
+
line1_hash=$(printf '%s' "$line1" | tr -d '\n' | compute_sha256)
|
|
639
|
+
line2_prev=$(printf '%s' "$line2" | python3 -c "import sys, json; print(json.load(sys.stdin).get('prev_hash', ''))")
|
|
640
|
+
|
|
641
|
+
if [ "$line1_hash" != "$line2_prev" ]; then
|
|
642
|
+
printf 'FAIL: Second event prev_hash does not match first line hash\n'
|
|
643
|
+
printf 'Expected: %s\n' "$line1_hash"
|
|
644
|
+
printf 'Got: %s\n' "$line2_prev"
|
|
645
|
+
exit 1
|
|
646
|
+
fi
|
|
647
|
+
|
|
648
|
+
printf 'PASS: Hash chain builds correctly across 2 events\n'
|
|
649
|
+
)
|
|
650
|
+
if [ $? -eq 0 ]; then
|
|
651
|
+
test_passed=$((test_passed + 1))
|
|
652
|
+
else
|
|
653
|
+
test_failed=$((test_failed + 1))
|
|
654
|
+
fi
|
|
655
|
+
|
|
656
|
+
printf '\n=== Test 10: verify-audit-log passes on intact chain ===\n'
|
|
657
|
+
(
|
|
658
|
+
export AESOP_ROOT="$tmpdir/aesop_verify"
|
|
659
|
+
mkdir -p "$AESOP_ROOT/state"
|
|
660
|
+
log_block "entry_1"
|
|
661
|
+
log_block "entry_2"
|
|
662
|
+
log_event "entry_3"
|
|
663
|
+
|
|
664
|
+
if [ ! -f "$AESOP_ROOT/state/SECURITY-AUDIT.log" ]; then
|
|
665
|
+
printf 'FAIL: Audit log not created\n'
|
|
666
|
+
exit 1
|
|
667
|
+
fi
|
|
668
|
+
|
|
669
|
+
if verify_audit_log "$AESOP_ROOT/state/SECURITY-AUDIT.log" >/dev/null 2>&1; then
|
|
670
|
+
printf 'PASS: Verification passed on intact chain\n'
|
|
671
|
+
else
|
|
672
|
+
printf 'FAIL: Verification should pass on intact chain\n'
|
|
673
|
+
exit 1
|
|
674
|
+
fi
|
|
675
|
+
)
|
|
676
|
+
if [ $? -eq 0 ]; then
|
|
677
|
+
test_passed=$((test_passed + 1))
|
|
678
|
+
else
|
|
679
|
+
test_failed=$((test_failed + 1))
|
|
680
|
+
fi
|
|
681
|
+
|
|
682
|
+
printf '\n=== Test 11: verify-audit-log detects tampered line ===\n'
|
|
683
|
+
(
|
|
684
|
+
export AESOP_ROOT="$tmpdir/aesop_tamper"
|
|
685
|
+
mkdir -p "$AESOP_ROOT/state"
|
|
686
|
+
log_block "entry_1"
|
|
687
|
+
log_block "entry_2"
|
|
688
|
+
log_block "entry_3"
|
|
689
|
+
|
|
690
|
+
if [ ! -f "$AESOP_ROOT/state/SECURITY-AUDIT.log" ]; then
|
|
691
|
+
printf 'FAIL: Audit log not created\n'
|
|
692
|
+
exit 1
|
|
693
|
+
fi
|
|
694
|
+
|
|
695
|
+
sed -i '2s/"reason":"[^"]*"/"reason":"TAMPERED"/g' "$AESOP_ROOT/state/SECURITY-AUDIT.log"
|
|
696
|
+
|
|
697
|
+
if ! verify_audit_log "$AESOP_ROOT/state/SECURITY-AUDIT.log" >/dev/null 2>&1; then
|
|
698
|
+
printf 'PASS: Verification detected tampered middle line\n'
|
|
699
|
+
else
|
|
700
|
+
printf 'FAIL: Verification should detect tampering\n'
|
|
701
|
+
exit 1
|
|
702
|
+
fi
|
|
703
|
+
)
|
|
704
|
+
if [ $? -eq 0 ]; then
|
|
705
|
+
test_passed=$((test_passed + 1))
|
|
706
|
+
else
|
|
707
|
+
test_failed=$((test_failed + 1))
|
|
708
|
+
fi
|
|
709
|
+
|
|
710
|
+
printf '\n=== Test Results ===\n'
|
|
711
|
+
printf 'PASSED: %d\n' "$test_passed"
|
|
712
|
+
printf 'FAILED: %d\n' "$test_failed"
|
|
713
|
+
|
|
714
|
+
if [ "$test_failed" -eq 0 ]; then
|
|
715
|
+
printf '\nAll 11 tests passed.\n'
|
|
716
|
+
return 0
|
|
717
|
+
else
|
|
718
|
+
printf '\nSome tests failed.\n'
|
|
719
|
+
return 1
|
|
720
|
+
fi
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
main() {
|
|
724
|
+
if [ "${1:-}" = "--test" ]; then
|
|
725
|
+
run_test_mode
|
|
726
|
+
exit $?
|
|
727
|
+
fi
|
|
728
|
+
|
|
729
|
+
if [ "${1:-}" = "--verify-audit-log" ]; then
|
|
730
|
+
local audit_log="${2:-${AESOP_ROOT:-$HOME/aesop}/state/SECURITY-AUDIT.log}"
|
|
731
|
+
verify_audit_log "$audit_log"
|
|
732
|
+
exit $?
|
|
733
|
+
fi
|
|
734
|
+
|
|
735
|
+
# git pre-push provides ref info on stdin, pass it to check_branch_policy
|
|
736
|
+
if ! check_branch_policy; then
|
|
737
|
+
printf 'Error: Push to main/master is blocked by policy\n' >&2
|
|
738
|
+
log_block "push_to_protected_branch"
|
|
739
|
+
exit 1
|
|
740
|
+
fi
|
|
741
|
+
|
|
742
|
+
if ! check_secret_scan; then
|
|
743
|
+
printf 'Error: Secret scan failed. Push blocked.\n' >&2
|
|
744
|
+
log_block "secret_scan_failure"
|
|
745
|
+
exit 1
|
|
746
|
+
fi
|
|
747
|
+
|
|
748
|
+
exit 0
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
752
|
+
main "$@"
|
|
753
|
+
fi
|