shipwright-cli 2.3.1 → 3.0.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 +95 -28
- package/completions/_shipwright +1 -1
- package/completions/shipwright.bash +3 -8
- package/completions/shipwright.fish +1 -1
- package/config/defaults.json +111 -0
- package/config/event-schema.json +81 -0
- package/config/policy.json +155 -2
- package/config/policy.schema.json +162 -1
- package/dashboard/coverage/coverage-summary.json +14 -0
- package/dashboard/public/index.html +1 -1
- package/dashboard/server.ts +306 -17
- package/dashboard/src/components/charts/bar.test.ts +79 -0
- package/dashboard/src/components/charts/donut.test.ts +68 -0
- package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
- package/dashboard/src/components/charts/sparkline.test.ts +125 -0
- package/dashboard/src/core/api.test.ts +309 -0
- package/dashboard/src/core/helpers.test.ts +301 -0
- package/dashboard/src/core/router.test.ts +307 -0
- package/dashboard/src/core/router.ts +7 -0
- package/dashboard/src/core/sse.test.ts +144 -0
- package/dashboard/src/views/metrics.test.ts +186 -0
- package/dashboard/src/views/overview.test.ts +173 -0
- package/dashboard/src/views/pipelines.test.ts +183 -0
- package/dashboard/src/views/team.test.ts +253 -0
- package/dashboard/vitest.config.ts +14 -5
- package/docs/TIPS.md +1 -1
- package/docs/patterns/README.md +1 -1
- package/package.json +15 -5
- package/scripts/adapters/docker-deploy.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +11 -1
- package/scripts/adapters/wezterm-adapter.sh +1 -1
- package/scripts/check-version-consistency.sh +1 -1
- package/scripts/lib/architecture.sh +126 -0
- package/scripts/lib/bootstrap.sh +75 -0
- package/scripts/lib/compat.sh +89 -6
- package/scripts/lib/config.sh +91 -0
- package/scripts/lib/daemon-adaptive.sh +3 -3
- package/scripts/lib/daemon-dispatch.sh +39 -16
- package/scripts/lib/daemon-health.sh +1 -1
- package/scripts/lib/daemon-patrol.sh +24 -12
- package/scripts/lib/daemon-poll.sh +37 -25
- package/scripts/lib/daemon-state.sh +115 -23
- package/scripts/lib/daemon-triage.sh +30 -8
- package/scripts/lib/fleet-failover.sh +63 -0
- package/scripts/lib/helpers.sh +30 -6
- package/scripts/lib/pipeline-detection.sh +2 -2
- package/scripts/lib/pipeline-github.sh +9 -9
- package/scripts/lib/pipeline-intelligence.sh +85 -35
- package/scripts/lib/pipeline-quality-checks.sh +16 -16
- package/scripts/lib/pipeline-quality.sh +1 -1
- package/scripts/lib/pipeline-stages.sh +242 -28
- package/scripts/lib/pipeline-state.sh +40 -4
- package/scripts/lib/test-helpers.sh +247 -0
- package/scripts/postinstall.mjs +3 -11
- package/scripts/sw +10 -4
- package/scripts/sw-activity.sh +1 -11
- package/scripts/sw-adaptive.sh +109 -85
- package/scripts/sw-adversarial.sh +4 -14
- package/scripts/sw-architecture-enforcer.sh +1 -11
- package/scripts/sw-auth.sh +8 -17
- package/scripts/sw-autonomous.sh +111 -49
- package/scripts/sw-changelog.sh +1 -11
- package/scripts/sw-checkpoint.sh +144 -20
- package/scripts/sw-ci.sh +2 -12
- package/scripts/sw-cleanup.sh +13 -17
- package/scripts/sw-code-review.sh +16 -36
- package/scripts/sw-connect.sh +5 -12
- package/scripts/sw-context.sh +9 -26
- package/scripts/sw-cost.sh +6 -16
- package/scripts/sw-daemon.sh +75 -70
- package/scripts/sw-dashboard.sh +57 -17
- package/scripts/sw-db.sh +506 -15
- package/scripts/sw-decompose.sh +1 -11
- package/scripts/sw-deps.sh +15 -25
- package/scripts/sw-developer-simulation.sh +1 -11
- package/scripts/sw-discovery.sh +112 -30
- package/scripts/sw-doc-fleet.sh +7 -17
- package/scripts/sw-docs-agent.sh +6 -16
- package/scripts/sw-docs.sh +4 -12
- package/scripts/sw-doctor.sh +134 -43
- package/scripts/sw-dora.sh +11 -19
- package/scripts/sw-durable.sh +35 -52
- package/scripts/sw-e2e-orchestrator.sh +11 -27
- package/scripts/sw-eventbus.sh +115 -115
- package/scripts/sw-evidence.sh +748 -0
- package/scripts/sw-feedback.sh +3 -13
- package/scripts/sw-fix.sh +2 -20
- package/scripts/sw-fleet-discover.sh +1 -11
- package/scripts/sw-fleet-viz.sh +10 -18
- package/scripts/sw-fleet.sh +13 -17
- package/scripts/sw-github-app.sh +6 -16
- package/scripts/sw-github-checks.sh +1 -11
- package/scripts/sw-github-deploy.sh +1 -11
- package/scripts/sw-github-graphql.sh +2 -12
- package/scripts/sw-guild.sh +1 -11
- package/scripts/sw-heartbeat.sh +49 -12
- package/scripts/sw-hygiene.sh +45 -43
- package/scripts/sw-incident.sh +284 -67
- package/scripts/sw-init.sh +35 -37
- package/scripts/sw-instrument.sh +1 -11
- package/scripts/sw-intelligence.sh +362 -51
- package/scripts/sw-jira.sh +5 -14
- package/scripts/sw-launchd.sh +2 -12
- package/scripts/sw-linear.sh +8 -17
- package/scripts/sw-logs.sh +4 -12
- package/scripts/sw-loop.sh +641 -90
- package/scripts/sw-memory.sh +243 -17
- package/scripts/sw-mission-control.sh +2 -12
- package/scripts/sw-model-router.sh +73 -34
- package/scripts/sw-otel.sh +11 -21
- package/scripts/sw-oversight.sh +1 -11
- package/scripts/sw-patrol-meta.sh +5 -11
- package/scripts/sw-pipeline-composer.sh +7 -17
- package/scripts/sw-pipeline-vitals.sh +1 -11
- package/scripts/sw-pipeline.sh +478 -122
- package/scripts/sw-pm.sh +2 -12
- package/scripts/sw-pr-lifecycle.sh +203 -29
- package/scripts/sw-predictive.sh +16 -22
- package/scripts/sw-prep.sh +6 -16
- package/scripts/sw-ps.sh +1 -11
- package/scripts/sw-public-dashboard.sh +2 -12
- package/scripts/sw-quality.sh +77 -10
- package/scripts/sw-reaper.sh +1 -11
- package/scripts/sw-recruit.sh +15 -25
- package/scripts/sw-regression.sh +11 -21
- package/scripts/sw-release-manager.sh +19 -28
- package/scripts/sw-release.sh +8 -16
- package/scripts/sw-remote.sh +1 -11
- package/scripts/sw-replay.sh +48 -44
- package/scripts/sw-retro.sh +70 -92
- package/scripts/sw-review-rerun.sh +220 -0
- package/scripts/sw-scale.sh +109 -32
- package/scripts/sw-security-audit.sh +12 -22
- package/scripts/sw-self-optimize.sh +239 -23
- package/scripts/sw-session.sh +3 -13
- package/scripts/sw-setup.sh +8 -18
- package/scripts/sw-standup.sh +5 -15
- package/scripts/sw-status.sh +32 -23
- package/scripts/sw-strategic.sh +129 -13
- package/scripts/sw-stream.sh +1 -11
- package/scripts/sw-swarm.sh +76 -36
- package/scripts/sw-team-stages.sh +10 -20
- package/scripts/sw-templates.sh +4 -14
- package/scripts/sw-testgen.sh +3 -13
- package/scripts/sw-tmux-pipeline.sh +1 -19
- package/scripts/sw-tmux-role-color.sh +0 -10
- package/scripts/sw-tmux-status.sh +3 -11
- package/scripts/sw-tmux.sh +2 -20
- package/scripts/sw-trace.sh +1 -19
- package/scripts/sw-tracker-github.sh +0 -10
- package/scripts/sw-tracker-jira.sh +1 -11
- package/scripts/sw-tracker-linear.sh +1 -11
- package/scripts/sw-tracker.sh +7 -24
- package/scripts/sw-triage.sh +24 -34
- package/scripts/sw-upgrade.sh +5 -23
- package/scripts/sw-ux.sh +1 -19
- package/scripts/sw-webhook.sh +18 -32
- package/scripts/sw-widgets.sh +3 -21
- package/scripts/sw-worktree.sh +11 -27
- package/scripts/update-homebrew-sha.sh +67 -0
- package/templates/pipelines/tdd.json +72 -0
- package/scripts/sw-pipeline.sh.mock +0 -7
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright evidence — Machine-Verifiable Proof for Agent Deliveries ║
|
|
4
|
+
# ║ Browser · API · Database · CLI · Webhook · Custom collectors ║
|
|
5
|
+
# ║ Capture · Verify · Manifest assertions · Artifact freshness ║
|
|
6
|
+
# ║ Part of the Code Factory pattern for deterministic merge evidence ║
|
|
7
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
10
|
+
|
|
11
|
+
VERSION="3.0.0"
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
14
|
+
|
|
15
|
+
# shellcheck source=lib/compat.sh
|
|
16
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
17
|
+
# shellcheck source=lib/helpers.sh
|
|
18
|
+
[[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
|
|
19
|
+
[[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
|
|
20
|
+
[[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
|
|
21
|
+
[[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
|
|
22
|
+
[[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
|
|
23
|
+
[[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
|
|
24
|
+
if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
|
|
25
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
26
|
+
now_epoch() { date +%s; }
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Cross-platform timeout: macOS lacks GNU timeout
|
|
30
|
+
_run_with_timeout() {
|
|
31
|
+
local secs="$1"; shift
|
|
32
|
+
if command -v gtimeout >/dev/null 2>&1; then
|
|
33
|
+
gtimeout "$secs" "$@"
|
|
34
|
+
elif command -v timeout >/dev/null 2>&1; then
|
|
35
|
+
timeout "$secs" "$@"
|
|
36
|
+
else
|
|
37
|
+
# Fallback: run without timeout
|
|
38
|
+
"$@"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
EVIDENCE_DIR="${REPO_DIR}/.claude/evidence"
|
|
43
|
+
MANIFEST_FILE="${EVIDENCE_DIR}/manifest.json"
|
|
44
|
+
POLICY_FILE="${REPO_DIR}/config/policy.json"
|
|
45
|
+
|
|
46
|
+
ensure_evidence_dir() {
|
|
47
|
+
mkdir -p "$EVIDENCE_DIR"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# ─── Policy Accessors ────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
get_collectors() {
|
|
53
|
+
local type_filter="${1:-}"
|
|
54
|
+
if [[ -f "$POLICY_FILE" ]]; then
|
|
55
|
+
if [[ -n "$type_filter" ]]; then
|
|
56
|
+
jq -c ".evidence.collectors[]? | select(.type == \"${type_filter}\")" "$POLICY_FILE" 2>/dev/null
|
|
57
|
+
else
|
|
58
|
+
jq -c '.evidence.collectors[]?' "$POLICY_FILE" 2>/dev/null
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get_max_age_minutes() {
|
|
64
|
+
if [[ -f "$POLICY_FILE" ]]; then
|
|
65
|
+
jq -r '.evidence.artifactMaxAgeMinutes // 30' "$POLICY_FILE" 2>/dev/null
|
|
66
|
+
else
|
|
67
|
+
echo "30"
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get_require_fresh() {
|
|
72
|
+
if [[ -f "$POLICY_FILE" ]]; then
|
|
73
|
+
jq -r '.evidence.requireFreshArtifacts // true' "$POLICY_FILE" 2>/dev/null
|
|
74
|
+
else
|
|
75
|
+
echo "true"
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
80
|
+
# ASSERTION EVALUATION
|
|
81
|
+
# Checks response body against assertions defined in policy.json.
|
|
82
|
+
# Assertion names map to simple content checks (case-insensitive).
|
|
83
|
+
# Returns the count of failed assertions (0 = all passed).
|
|
84
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
85
|
+
|
|
86
|
+
evaluate_assertions() {
|
|
87
|
+
local collector_json="$1"
|
|
88
|
+
local response_body="$2"
|
|
89
|
+
local failed=0
|
|
90
|
+
|
|
91
|
+
local assertions
|
|
92
|
+
assertions=$(echo "$collector_json" | jq -r '.assertions[]? // empty' 2>/dev/null)
|
|
93
|
+
[[ -z "$assertions" ]] && { echo "0"; return; }
|
|
94
|
+
|
|
95
|
+
while IFS= read -r assertion; do
|
|
96
|
+
[[ -z "$assertion" ]] && continue
|
|
97
|
+
local check_passed="false"
|
|
98
|
+
|
|
99
|
+
case "$assertion" in
|
|
100
|
+
# Common assertion patterns — map names to body content checks
|
|
101
|
+
page-title-visible)
|
|
102
|
+
echo "$response_body" | grep -qi '<title>' && check_passed="true" ;;
|
|
103
|
+
websocket-connected|websocket-active)
|
|
104
|
+
echo "$response_body" | grep -qi 'websocket\|ws://' && check_passed="true" ;;
|
|
105
|
+
status-ok)
|
|
106
|
+
echo "$response_body" | grep -qi '"status"' && check_passed="true" ;;
|
|
107
|
+
response-has-version)
|
|
108
|
+
echo "$response_body" | grep -qi '"version"' && check_passed="true" ;;
|
|
109
|
+
valid-json-output|valid-json)
|
|
110
|
+
echo "$response_body" | jq empty 2>/dev/null && check_passed="true" ;;
|
|
111
|
+
has-pipeline-state)
|
|
112
|
+
echo "$response_body" | grep -qi 'pipeline\|status\|stage' && check_passed="true" ;;
|
|
113
|
+
stage-list-rendered)
|
|
114
|
+
echo "$response_body" | grep -qi 'stage\|pipeline' && check_passed="true" ;;
|
|
115
|
+
progress-indicator-visible)
|
|
116
|
+
echo "$response_body" | grep -qi 'progress\|percent\|stage' && check_passed="true" ;;
|
|
117
|
+
schema-valid|db-accessible)
|
|
118
|
+
echo "$response_body" | grep -qi 'schema\|version\|ok\|healthy' && check_passed="true" ;;
|
|
119
|
+
*)
|
|
120
|
+
# Generic: check if the assertion name (with hyphens as spaces) appears in body
|
|
121
|
+
local search_term="${assertion//-/ }"
|
|
122
|
+
echo "$response_body" | grep -qi "$search_term" && check_passed="true" ;;
|
|
123
|
+
esac
|
|
124
|
+
|
|
125
|
+
if [[ "$check_passed" != "true" ]]; then
|
|
126
|
+
warn "[assertion] '${assertion}' not satisfied" >&2
|
|
127
|
+
failed=$((failed + 1))
|
|
128
|
+
fi
|
|
129
|
+
done <<< "$assertions"
|
|
130
|
+
|
|
131
|
+
echo "$failed"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
135
|
+
# TYPE-SPECIFIC COLLECTORS
|
|
136
|
+
# Each returns a JSON evidence record written to EVIDENCE_DIR/<name>.json
|
|
137
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
138
|
+
|
|
139
|
+
# ─── Browser: HTTP page load against a URL path ──────────────────────────────
|
|
140
|
+
|
|
141
|
+
collect_browser() {
|
|
142
|
+
local name="$1"
|
|
143
|
+
local collector_json="$2"
|
|
144
|
+
|
|
145
|
+
local entrypoint base_url url
|
|
146
|
+
entrypoint=$(echo "$collector_json" | jq -r '.entrypoint // "/"')
|
|
147
|
+
base_url=$(echo "$collector_json" | jq -r '.baseUrl // ""')
|
|
148
|
+
[[ -z "$base_url" ]] && base_url="http://localhost:$(_config_get_int "dashboard.port" 8767)"
|
|
149
|
+
url="${base_url}${entrypoint}"
|
|
150
|
+
|
|
151
|
+
info "[browser] ${name}: ${url}"
|
|
152
|
+
|
|
153
|
+
local http_status="0"
|
|
154
|
+
local response_size="0"
|
|
155
|
+
local response_body=""
|
|
156
|
+
|
|
157
|
+
if command -v curl >/dev/null 2>&1; then
|
|
158
|
+
local tmpfile
|
|
159
|
+
tmpfile=$(mktemp "${TMPDIR:-/tmp}/sw-evidence-browser.XXXXXX")
|
|
160
|
+
http_status=$(curl -s -o "$tmpfile" -w "%{http_code}" --max-time 30 "$url" 2>/dev/null || echo "0")
|
|
161
|
+
if [[ -f "$tmpfile" ]]; then
|
|
162
|
+
response_size=$(wc -c < "$tmpfile" 2>/dev/null || echo "0")
|
|
163
|
+
response_body=$(cat "$tmpfile" 2>/dev/null || echo "")
|
|
164
|
+
rm -f "$tmpfile"
|
|
165
|
+
fi
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
local passed="false"
|
|
169
|
+
[[ "$http_status" -ge 200 && "$http_status" -lt 400 ]] && passed="true"
|
|
170
|
+
|
|
171
|
+
# Evaluate assertions against response body (if status check passed)
|
|
172
|
+
local assertion_failures=0
|
|
173
|
+
if [[ "$passed" == "true" && -n "$response_body" ]]; then
|
|
174
|
+
assertion_failures=$(evaluate_assertions "$collector_json" "$response_body")
|
|
175
|
+
[[ "$assertion_failures" -gt 0 ]] && passed="false"
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
write_evidence_record "$name" "browser" "$passed" \
|
|
179
|
+
"$(jq -n --arg url "$url" --argjson status "$http_status" --argjson size "$response_size" \
|
|
180
|
+
--argjson assertion_failures "$assertion_failures" \
|
|
181
|
+
'{url: $url, http_status: $status, response_size: $size, assertion_failures: $assertion_failures}')"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# ─── API: REST/GraphQL endpoint verification ─────────────────────────────────
|
|
185
|
+
|
|
186
|
+
collect_api() {
|
|
187
|
+
local name="$1"
|
|
188
|
+
local collector_json="$2"
|
|
189
|
+
|
|
190
|
+
local url method expected_status headers_json body timeout
|
|
191
|
+
url=$(echo "$collector_json" | jq -r '.url // ""')
|
|
192
|
+
method=$(echo "$collector_json" | jq -r '.method // "GET"')
|
|
193
|
+
expected_status=$(echo "$collector_json" | jq -r '.expectedStatus // 200')
|
|
194
|
+
body=$(echo "$collector_json" | jq -r '.body // ""')
|
|
195
|
+
timeout=$(echo "$collector_json" | jq -r '.timeout // 30')
|
|
196
|
+
|
|
197
|
+
if [[ -z "$url" ]]; then
|
|
198
|
+
local base_url entrypoint
|
|
199
|
+
base_url=$(echo "$collector_json" | jq -r '.baseUrl // ""')
|
|
200
|
+
[[ -z "$base_url" ]] && base_url="http://localhost:$(_config_get_int "dashboard.port" 8767)"
|
|
201
|
+
entrypoint=$(echo "$collector_json" | jq -r '.entrypoint // "/"')
|
|
202
|
+
url="${base_url}${entrypoint}"
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
info "[api] ${name}: ${method} ${url}"
|
|
206
|
+
|
|
207
|
+
local http_status="0"
|
|
208
|
+
local response_size="0"
|
|
209
|
+
local response_body=""
|
|
210
|
+
local content_type=""
|
|
211
|
+
|
|
212
|
+
if command -v curl >/dev/null 2>&1; then
|
|
213
|
+
local tmpfile header_file
|
|
214
|
+
tmpfile=$(mktemp "${TMPDIR:-/tmp}/sw-evidence-api.XXXXXX")
|
|
215
|
+
header_file=$(mktemp "${TMPDIR:-/tmp}/sw-evidence-api-headers.XXXXXX")
|
|
216
|
+
local curl_args=(-s -o "$tmpfile" -D "$header_file" -w "%{http_code}" -X "$method" --max-time "$timeout")
|
|
217
|
+
|
|
218
|
+
# Add custom headers
|
|
219
|
+
local custom_headers
|
|
220
|
+
custom_headers=$(echo "$collector_json" | jq -r '.headers // {} | to_entries[] | "-H\n\(.key): \(.value)"' 2>/dev/null || true)
|
|
221
|
+
if [[ -n "$custom_headers" ]]; then
|
|
222
|
+
while IFS= read -r line; do
|
|
223
|
+
[[ "$line" == "-H" ]] && continue
|
|
224
|
+
curl_args+=(-H "$line")
|
|
225
|
+
done <<< "$custom_headers"
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
# Add body for POST/PUT/PATCH
|
|
229
|
+
if [[ -n "$body" && "$method" != "GET" && "$method" != "HEAD" ]]; then
|
|
230
|
+
curl_args+=(-d "$body")
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
http_status=$(curl "${curl_args[@]}" "$url" 2>/dev/null || echo "0")
|
|
234
|
+
|
|
235
|
+
if [[ -f "$tmpfile" ]]; then
|
|
236
|
+
response_size=$(wc -c < "$tmpfile" 2>/dev/null || echo "0")
|
|
237
|
+
response_body=$(cat "$tmpfile" 2>/dev/null || echo "")
|
|
238
|
+
rm -f "$tmpfile"
|
|
239
|
+
fi
|
|
240
|
+
if [[ -f "$header_file" ]]; then
|
|
241
|
+
content_type=$(grep -i "^content-type:" "$header_file" 2>/dev/null | head -1 | sed 's/^[^:]*: *//' | tr -d '\r' || echo "")
|
|
242
|
+
rm -f "$header_file"
|
|
243
|
+
fi
|
|
244
|
+
rm -f "$tmpfile"
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
local passed="false"
|
|
248
|
+
[[ "$http_status" -eq "$expected_status" ]] && passed="true"
|
|
249
|
+
|
|
250
|
+
# Check if response is valid JSON when content-type suggests it
|
|
251
|
+
local valid_json="false"
|
|
252
|
+
if echo "$response_body" | jq empty 2>/dev/null; then
|
|
253
|
+
valid_json="true"
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
# Evaluate assertions against response body (if status check passed)
|
|
257
|
+
local assertion_failures=0
|
|
258
|
+
if [[ "$passed" == "true" && -n "$response_body" ]]; then
|
|
259
|
+
assertion_failures=$(evaluate_assertions "$collector_json" "$response_body")
|
|
260
|
+
[[ "$assertion_failures" -gt 0 ]] && passed="false"
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
write_evidence_record "$name" "api" "$passed" \
|
|
264
|
+
"$(jq -n --arg url "$url" --arg method "$method" \
|
|
265
|
+
--argjson status "$http_status" --argjson expected "$expected_status" \
|
|
266
|
+
--argjson size "$response_size" --arg content_type "$content_type" \
|
|
267
|
+
--arg valid_json "$valid_json" --argjson assertion_failures "$assertion_failures" \
|
|
268
|
+
'{url: $url, method: $method, http_status: $status, expected_status: $expected, response_size: $size, content_type: $content_type, valid_json: ($valid_json == "true"), assertion_failures: $assertion_failures}')"
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# ─── CLI: Execute a command and check exit code ──────────────────────────────
|
|
272
|
+
|
|
273
|
+
collect_cli() {
|
|
274
|
+
local name="$1"
|
|
275
|
+
local collector_json="$2"
|
|
276
|
+
|
|
277
|
+
local command_str expected_exit timeout
|
|
278
|
+
command_str=$(echo "$collector_json" | jq -r '.command // ""')
|
|
279
|
+
expected_exit=$(echo "$collector_json" | jq -r '.expectedExitCode // 0')
|
|
280
|
+
timeout=$(echo "$collector_json" | jq -r '.timeout // 60')
|
|
281
|
+
|
|
282
|
+
if [[ -z "$command_str" ]]; then
|
|
283
|
+
error "[cli] ${name}: no command specified"
|
|
284
|
+
write_evidence_record "$name" "cli" "false" '{"error": "no command specified"}'
|
|
285
|
+
return
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
info "[cli] ${name}: ${command_str}"
|
|
289
|
+
|
|
290
|
+
local exit_code=0
|
|
291
|
+
local output=""
|
|
292
|
+
local start_time
|
|
293
|
+
start_time=$(date +%s)
|
|
294
|
+
|
|
295
|
+
output=$(cd "$REPO_DIR" && _run_with_timeout "$timeout" bash -c "$command_str" 2>&1) || exit_code=$?
|
|
296
|
+
|
|
297
|
+
local elapsed=$(( $(date +%s) - start_time ))
|
|
298
|
+
|
|
299
|
+
local passed="false"
|
|
300
|
+
[[ "$exit_code" -eq "$expected_exit" ]] && passed="true"
|
|
301
|
+
|
|
302
|
+
local valid_json="false"
|
|
303
|
+
if echo "$output" | jq empty 2>/dev/null; then
|
|
304
|
+
valid_json="true"
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# Evaluate assertions against command output (if exit code check passed)
|
|
308
|
+
local assertion_failures=0
|
|
309
|
+
if [[ "$passed" == "true" && -n "$output" ]]; then
|
|
310
|
+
assertion_failures=$(evaluate_assertions "$collector_json" "$output")
|
|
311
|
+
[[ "$assertion_failures" -gt 0 ]] && passed="false"
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
local output_size=${#output}
|
|
315
|
+
# Truncate output for the evidence record (keep first 2000 chars)
|
|
316
|
+
local output_preview="${output:0:2000}"
|
|
317
|
+
|
|
318
|
+
write_evidence_record "$name" "cli" "$passed" \
|
|
319
|
+
"$(jq -n --arg cmd "$command_str" --argjson exit_code "$exit_code" \
|
|
320
|
+
--argjson expected "$expected_exit" --argjson elapsed "$elapsed" \
|
|
321
|
+
--argjson output_size "$output_size" --arg valid_json "$valid_json" \
|
|
322
|
+
--arg output_preview "$output_preview" \
|
|
323
|
+
'{command: $cmd, exit_code: $exit_code, expected_exit_code: $expected, elapsed_seconds: $elapsed, output_size: $output_size, valid_json: ($valid_json == "true"), output_preview: $output_preview}')"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# ─── Database: Schema/migration check via command ─────────────────────────────
|
|
327
|
+
|
|
328
|
+
collect_database() {
|
|
329
|
+
local name="$1"
|
|
330
|
+
local collector_json="$2"
|
|
331
|
+
|
|
332
|
+
local command_str expected_exit timeout
|
|
333
|
+
command_str=$(echo "$collector_json" | jq -r '.command // ""')
|
|
334
|
+
expected_exit=$(echo "$collector_json" | jq -r '.expectedExitCode // 0')
|
|
335
|
+
timeout=$(echo "$collector_json" | jq -r '.timeout // 30')
|
|
336
|
+
|
|
337
|
+
if [[ -z "$command_str" ]]; then
|
|
338
|
+
error "[database] ${name}: no command specified"
|
|
339
|
+
write_evidence_record "$name" "database" "false" '{"error": "no command specified"}'
|
|
340
|
+
return
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
info "[database] ${name}: ${command_str}"
|
|
344
|
+
|
|
345
|
+
local exit_code=0
|
|
346
|
+
local output=""
|
|
347
|
+
output=$(cd "$REPO_DIR" && _run_with_timeout "$timeout" bash -c "$command_str" 2>&1) || exit_code=$?
|
|
348
|
+
|
|
349
|
+
local passed="false"
|
|
350
|
+
[[ "$exit_code" -eq "$expected_exit" ]] && passed="true"
|
|
351
|
+
|
|
352
|
+
# Evaluate assertions against command output (if exit code check passed)
|
|
353
|
+
local assertion_failures=0
|
|
354
|
+
if [[ "$passed" == "true" && -n "$output" ]]; then
|
|
355
|
+
assertion_failures=$(evaluate_assertions "$collector_json" "$output")
|
|
356
|
+
[[ "$assertion_failures" -gt 0 ]] && passed="false"
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
local output_preview="${output:0:2000}"
|
|
360
|
+
|
|
361
|
+
write_evidence_record "$name" "database" "$passed" \
|
|
362
|
+
"$(jq -n --arg cmd "$command_str" --argjson exit_code "$exit_code" \
|
|
363
|
+
--argjson expected "$expected_exit" --arg output_preview "$output_preview" \
|
|
364
|
+
--argjson assertion_failures "$assertion_failures" \
|
|
365
|
+
'{command: $cmd, exit_code: $exit_code, expected_exit_code: $expected, output_preview: $output_preview, assertion_failures: $assertion_failures}')"
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# ─── Webhook: Issue a callback and verify response ───────────────────────────
|
|
369
|
+
|
|
370
|
+
collect_webhook() {
|
|
371
|
+
local name="$1"
|
|
372
|
+
local collector_json="$2"
|
|
373
|
+
|
|
374
|
+
local url method expected_status body timeout
|
|
375
|
+
url=$(echo "$collector_json" | jq -r '.url // ""')
|
|
376
|
+
method=$(echo "$collector_json" | jq -r '.method // "POST"')
|
|
377
|
+
expected_status=$(echo "$collector_json" | jq -r '.expectedStatus // 200')
|
|
378
|
+
body=$(echo "$collector_json" | jq -r '.body // "{}"')
|
|
379
|
+
timeout=$(echo "$collector_json" | jq -r '.timeout // 15')
|
|
380
|
+
|
|
381
|
+
if [[ -z "$url" ]]; then
|
|
382
|
+
error "[webhook] ${name}: no URL specified"
|
|
383
|
+
write_evidence_record "$name" "webhook" "false" '{"error": "no URL specified"}'
|
|
384
|
+
return
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
info "[webhook] ${name}: ${method} ${url}"
|
|
388
|
+
|
|
389
|
+
local http_status="0"
|
|
390
|
+
local response_body=""
|
|
391
|
+
|
|
392
|
+
if command -v curl >/dev/null 2>&1; then
|
|
393
|
+
local tmpfile
|
|
394
|
+
tmpfile=$(mktemp "${TMPDIR:-/tmp}/sw-evidence-webhook.XXXXXX")
|
|
395
|
+
http_status=$(curl -s -o "$tmpfile" -w "%{http_code}" -X "$method" \
|
|
396
|
+
-H "Content-Type: application/json" -d "$body" \
|
|
397
|
+
--max-time "$timeout" "$url" 2>/dev/null || echo "0")
|
|
398
|
+
if [[ -f "$tmpfile" ]]; then
|
|
399
|
+
response_body=$(cat "$tmpfile" 2>/dev/null || echo "")
|
|
400
|
+
rm -f "$tmpfile"
|
|
401
|
+
fi
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
local passed="false"
|
|
405
|
+
[[ "$http_status" -eq "$expected_status" ]] && passed="true"
|
|
406
|
+
|
|
407
|
+
write_evidence_record "$name" "webhook" "$passed" \
|
|
408
|
+
"$(jq -n --arg url "$url" --arg method "$method" \
|
|
409
|
+
--argjson status "$http_status" --argjson expected "$expected_status" \
|
|
410
|
+
'{url: $url, method: $method, http_status: $status, expected_status: $expected}')"
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
# ─── Custom: User-defined script execution ───────────────────────────────────
|
|
414
|
+
|
|
415
|
+
collect_custom() {
|
|
416
|
+
local name="$1"
|
|
417
|
+
local collector_json="$2"
|
|
418
|
+
|
|
419
|
+
local command_str expected_exit timeout
|
|
420
|
+
command_str=$(echo "$collector_json" | jq -r '.command // ""')
|
|
421
|
+
expected_exit=$(echo "$collector_json" | jq -r '.expectedExitCode // 0')
|
|
422
|
+
timeout=$(echo "$collector_json" | jq -r '.timeout // 60')
|
|
423
|
+
|
|
424
|
+
if [[ -z "$command_str" ]]; then
|
|
425
|
+
error "[custom] ${name}: no command specified"
|
|
426
|
+
write_evidence_record "$name" "custom" "false" '{"error": "no command specified"}'
|
|
427
|
+
return
|
|
428
|
+
fi
|
|
429
|
+
|
|
430
|
+
info "[custom] ${name}: ${command_str}"
|
|
431
|
+
|
|
432
|
+
local exit_code=0
|
|
433
|
+
local output=""
|
|
434
|
+
output=$(cd "$REPO_DIR" && _run_with_timeout "$timeout" bash -c "$command_str" 2>&1) || exit_code=$?
|
|
435
|
+
|
|
436
|
+
local passed="false"
|
|
437
|
+
[[ "$exit_code" -eq "$expected_exit" ]] && passed="true"
|
|
438
|
+
|
|
439
|
+
local output_preview="${output:0:2000}"
|
|
440
|
+
|
|
441
|
+
write_evidence_record "$name" "custom" "$passed" \
|
|
442
|
+
"$(jq -n --arg cmd "$command_str" --argjson exit_code "$exit_code" \
|
|
443
|
+
--argjson expected "$expected_exit" --arg output_preview "$output_preview" \
|
|
444
|
+
'{command: $cmd, exit_code: $exit_code, expected_exit_code: $expected, output_preview: $output_preview}')"
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
448
|
+
# EVIDENCE RECORD WRITER
|
|
449
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
450
|
+
|
|
451
|
+
write_evidence_record() {
|
|
452
|
+
local name="$1"
|
|
453
|
+
local type="$2"
|
|
454
|
+
local passed="$3"
|
|
455
|
+
local details="$4"
|
|
456
|
+
|
|
457
|
+
local evidence_file="${EVIDENCE_DIR}/${name}.json"
|
|
458
|
+
local captured_at
|
|
459
|
+
captured_at=$(now_iso)
|
|
460
|
+
|
|
461
|
+
jq -n --arg name "$name" --arg type "$type" --arg passed "$passed" \
|
|
462
|
+
--arg captured_at "$captured_at" --argjson captured_epoch "$(now_epoch)" \
|
|
463
|
+
--argjson details "$details" \
|
|
464
|
+
'{
|
|
465
|
+
name: $name,
|
|
466
|
+
type: $type,
|
|
467
|
+
passed: ($passed == "true"),
|
|
468
|
+
captured_at: $captured_at,
|
|
469
|
+
captured_epoch: $captured_epoch,
|
|
470
|
+
details: $details
|
|
471
|
+
}' > "$evidence_file"
|
|
472
|
+
|
|
473
|
+
if [[ "$passed" == "true" ]]; then
|
|
474
|
+
success "[${type}] ${name}: passed"
|
|
475
|
+
else
|
|
476
|
+
error "[${type}] ${name}: failed"
|
|
477
|
+
fi
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
481
|
+
# COMMANDS
|
|
482
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
483
|
+
|
|
484
|
+
cmd_capture() {
|
|
485
|
+
local type_filter="${1:-}"
|
|
486
|
+
ensure_evidence_dir
|
|
487
|
+
|
|
488
|
+
info "Capturing evidence${type_filter:+ (type: ${type_filter})}..."
|
|
489
|
+
|
|
490
|
+
local collectors
|
|
491
|
+
collectors=$(get_collectors "$type_filter")
|
|
492
|
+
|
|
493
|
+
if [[ -z "$collectors" ]]; then
|
|
494
|
+
warn "No evidence collectors defined in policy — nothing to capture"
|
|
495
|
+
return 0
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
local total=0
|
|
499
|
+
local passed=0
|
|
500
|
+
local failed=0
|
|
501
|
+
local manifest_entries="[]"
|
|
502
|
+
|
|
503
|
+
while IFS= read -r collector; do
|
|
504
|
+
[[ -z "$collector" ]] && continue
|
|
505
|
+
|
|
506
|
+
local cname ctype
|
|
507
|
+
cname=$(echo "$collector" | jq -r '.name')
|
|
508
|
+
ctype=$(echo "$collector" | jq -r '.type')
|
|
509
|
+
|
|
510
|
+
case "$ctype" in
|
|
511
|
+
browser) collect_browser "$cname" "$collector" ;;
|
|
512
|
+
api) collect_api "$cname" "$collector" ;;
|
|
513
|
+
cli) collect_cli "$cname" "$collector" ;;
|
|
514
|
+
database) collect_database "$cname" "$collector" ;;
|
|
515
|
+
webhook) collect_webhook "$cname" "$collector" ;;
|
|
516
|
+
custom) collect_custom "$cname" "$collector" ;;
|
|
517
|
+
*) warn "Unknown collector type: ${ctype} (skipping ${cname})" ; continue ;;
|
|
518
|
+
esac
|
|
519
|
+
|
|
520
|
+
total=$((total + 1))
|
|
521
|
+
|
|
522
|
+
local evidence_file="${EVIDENCE_DIR}/${cname}.json"
|
|
523
|
+
local cpassed="false"
|
|
524
|
+
if [[ -f "$evidence_file" ]]; then
|
|
525
|
+
cpassed=$(jq -r '.passed' "$evidence_file" 2>/dev/null || echo "false")
|
|
526
|
+
fi
|
|
527
|
+
|
|
528
|
+
if [[ "$cpassed" == "true" ]]; then
|
|
529
|
+
passed=$((passed + 1))
|
|
530
|
+
else
|
|
531
|
+
failed=$((failed + 1))
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
manifest_entries=$(echo "$manifest_entries" | jq \
|
|
535
|
+
--arg name "$cname" --arg type "$ctype" --arg file "$evidence_file" --arg passed "$cpassed" \
|
|
536
|
+
'. + [{"name": $name, "type": $type, "file": $file, "passed": ($passed == "true")}]')
|
|
537
|
+
|
|
538
|
+
done <<< "$collectors"
|
|
539
|
+
|
|
540
|
+
# Write manifest
|
|
541
|
+
jq -n --arg captured_at "$(now_iso)" --argjson captured_epoch "$(now_epoch)" \
|
|
542
|
+
--argjson total "$total" --argjson passed "$passed" --argjson failed "$failed" \
|
|
543
|
+
--argjson collectors "$manifest_entries" \
|
|
544
|
+
'{
|
|
545
|
+
captured_at: $captured_at,
|
|
546
|
+
captured_epoch: $captured_epoch,
|
|
547
|
+
collector_count: $total,
|
|
548
|
+
passed: $passed,
|
|
549
|
+
failed: $failed,
|
|
550
|
+
collectors: $collectors
|
|
551
|
+
}' > "$MANIFEST_FILE"
|
|
552
|
+
|
|
553
|
+
echo ""
|
|
554
|
+
if [[ "$failed" -eq 0 ]]; then
|
|
555
|
+
success "All ${total} collector(s) passed"
|
|
556
|
+
else
|
|
557
|
+
warn "${passed}/${total} passed, ${failed} failed"
|
|
558
|
+
fi
|
|
559
|
+
|
|
560
|
+
emit_event "evidence.captured" "total=${total}" "passed=${passed}" "failed=${failed}" "type=${type_filter:-all}"
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
cmd_verify() {
|
|
564
|
+
ensure_evidence_dir
|
|
565
|
+
|
|
566
|
+
if [[ ! -f "$MANIFEST_FILE" ]]; then
|
|
567
|
+
error "No evidence manifest found — run 'capture' first"
|
|
568
|
+
return 1
|
|
569
|
+
fi
|
|
570
|
+
|
|
571
|
+
info "Verifying evidence..."
|
|
572
|
+
|
|
573
|
+
local all_passed="true"
|
|
574
|
+
local checked=0
|
|
575
|
+
local failed=0
|
|
576
|
+
|
|
577
|
+
# Check freshness
|
|
578
|
+
local require_fresh
|
|
579
|
+
require_fresh=$(get_require_fresh)
|
|
580
|
+
local max_age_minutes
|
|
581
|
+
max_age_minutes=$(get_max_age_minutes)
|
|
582
|
+
local max_age_seconds=$((max_age_minutes * 60))
|
|
583
|
+
|
|
584
|
+
local captured_epoch
|
|
585
|
+
captured_epoch=$(jq -r '.captured_epoch' "$MANIFEST_FILE" 2>/dev/null || echo "0")
|
|
586
|
+
local current_epoch
|
|
587
|
+
current_epoch=$(now_epoch)
|
|
588
|
+
local age_seconds=$((current_epoch - captured_epoch))
|
|
589
|
+
|
|
590
|
+
if [[ "$require_fresh" == "true" && "$age_seconds" -gt "$max_age_seconds" ]]; then
|
|
591
|
+
error "Evidence is stale: captured ${age_seconds}s ago (max: ${max_age_seconds}s)"
|
|
592
|
+
all_passed="false"
|
|
593
|
+
failed=$((failed + 1))
|
|
594
|
+
else
|
|
595
|
+
local age_minutes=$((age_seconds / 60))
|
|
596
|
+
info "Evidence age: ${age_minutes}m (max: ${max_age_minutes}m)"
|
|
597
|
+
fi
|
|
598
|
+
|
|
599
|
+
# Check all collectors in manifest
|
|
600
|
+
local collector_count
|
|
601
|
+
collector_count=$(jq -r '.collector_count' "$MANIFEST_FILE" 2>/dev/null || echo "0")
|
|
602
|
+
|
|
603
|
+
local collectors_json
|
|
604
|
+
collectors_json=$(jq -c '.collectors[]?' "$MANIFEST_FILE" 2>/dev/null)
|
|
605
|
+
|
|
606
|
+
while IFS= read -r entry; do
|
|
607
|
+
[[ -z "$entry" ]] && continue
|
|
608
|
+
checked=$((checked + 1))
|
|
609
|
+
|
|
610
|
+
local cname ctype cpassed
|
|
611
|
+
cname=$(echo "$entry" | jq -r '.name')
|
|
612
|
+
ctype=$(echo "$entry" | jq -r '.type')
|
|
613
|
+
cpassed=$(echo "$entry" | jq -r '.passed')
|
|
614
|
+
|
|
615
|
+
if [[ "$cpassed" != "true" ]]; then
|
|
616
|
+
error "Collector '${cname}' (${ctype}) failed"
|
|
617
|
+
all_passed="false"
|
|
618
|
+
failed=$((failed + 1))
|
|
619
|
+
else
|
|
620
|
+
success "Collector '${cname}' (${ctype}) passed"
|
|
621
|
+
fi
|
|
622
|
+
done <<< "$collectors_json"
|
|
623
|
+
|
|
624
|
+
echo ""
|
|
625
|
+
if [[ "$all_passed" == "true" ]]; then
|
|
626
|
+
success "All ${checked} evidence check(s) passed"
|
|
627
|
+
emit_event "evidence.verified" "total=${checked}" "result=pass"
|
|
628
|
+
return 0
|
|
629
|
+
else
|
|
630
|
+
error "${failed} of ${checked} evidence check(s) failed"
|
|
631
|
+
emit_event "evidence.verified" "total=${checked}" "result=fail" "failed=${failed}"
|
|
632
|
+
return 1
|
|
633
|
+
fi
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
cmd_pre_pr() {
|
|
637
|
+
local type_filter="${1:-}"
|
|
638
|
+
info "Running pre-PR evidence check..."
|
|
639
|
+
cmd_capture "$type_filter"
|
|
640
|
+
cmd_verify
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
cmd_status() {
|
|
644
|
+
ensure_evidence_dir
|
|
645
|
+
|
|
646
|
+
if [[ ! -f "$MANIFEST_FILE" ]]; then
|
|
647
|
+
warn "No evidence manifest found"
|
|
648
|
+
return 0
|
|
649
|
+
fi
|
|
650
|
+
|
|
651
|
+
local captured_at collector_count passed_count failed_count
|
|
652
|
+
captured_at=$(jq -r '.captured_at' "$MANIFEST_FILE" 2>/dev/null || echo "unknown")
|
|
653
|
+
collector_count=$(jq -r '.collector_count' "$MANIFEST_FILE" 2>/dev/null || echo "0")
|
|
654
|
+
passed_count=$(jq -r '.passed' "$MANIFEST_FILE" 2>/dev/null || echo "0")
|
|
655
|
+
failed_count=$(jq -r '.failed' "$MANIFEST_FILE" 2>/dev/null || echo "0")
|
|
656
|
+
|
|
657
|
+
echo "Evidence Status"
|
|
658
|
+
echo "━━━━━━━━━━━━━━━"
|
|
659
|
+
echo "Manifest: ${MANIFEST_FILE}"
|
|
660
|
+
echo "Captured at: ${captured_at}"
|
|
661
|
+
echo "Collectors: ${collector_count} (${passed_count} passed, ${failed_count} failed)"
|
|
662
|
+
echo ""
|
|
663
|
+
|
|
664
|
+
# Group by type
|
|
665
|
+
local types
|
|
666
|
+
types=$(jq -r '.collectors[].type' "$MANIFEST_FILE" 2>/dev/null | sort -u)
|
|
667
|
+
|
|
668
|
+
while IFS= read -r type; do
|
|
669
|
+
[[ -z "$type" ]] && continue
|
|
670
|
+
echo " ${type}:"
|
|
671
|
+
jq -r ".collectors[] | select(.type == \"${type}\") | \" \\(if .passed then \"✓\" else \"✗\" end) \\(.name)\"" "$MANIFEST_FILE" 2>/dev/null || true
|
|
672
|
+
done <<< "$types"
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
cmd_list_types() {
|
|
676
|
+
echo "Supported evidence types:"
|
|
677
|
+
echo ""
|
|
678
|
+
echo " browser HTTP page load — verifies UI renders correctly"
|
|
679
|
+
echo " api REST/GraphQL endpoint — verifies response status, body, content-type"
|
|
680
|
+
echo " database Schema/migration check — verifies DB integrity via command"
|
|
681
|
+
echo " cli Command execution — verifies exit code and output"
|
|
682
|
+
echo " webhook Callback verification — verifies webhook endpoint responds"
|
|
683
|
+
echo " custom User-defined script — any verification logic"
|
|
684
|
+
echo ""
|
|
685
|
+
echo "Configure collectors in config/policy.json under the 'evidence' section."
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
show_help() {
|
|
689
|
+
cat << 'EOF'
|
|
690
|
+
Usage: shipwright evidence <command> [args]
|
|
691
|
+
|
|
692
|
+
Commands:
|
|
693
|
+
capture [type] Capture evidence (optionally filter by type)
|
|
694
|
+
verify Verify evidence manifest and freshness
|
|
695
|
+
pre-pr [type] Capture + verify (run before PR creation)
|
|
696
|
+
status Show current evidence state grouped by type
|
|
697
|
+
types List supported evidence types
|
|
698
|
+
|
|
699
|
+
Evidence Types:
|
|
700
|
+
browser HTTP page load verification
|
|
701
|
+
api REST/GraphQL endpoint checks
|
|
702
|
+
database Schema/migration integrity
|
|
703
|
+
cli Command execution and exit code
|
|
704
|
+
webhook Callback endpoint verification
|
|
705
|
+
custom User-defined verification scripts
|
|
706
|
+
|
|
707
|
+
Evidence collectors are defined in config/policy.json under the
|
|
708
|
+
'evidence.collectors' array. Each collector specifies a type,
|
|
709
|
+
target, and assertions.
|
|
710
|
+
|
|
711
|
+
Part of the Code Factory pattern for machine-verifiable proof.
|
|
712
|
+
EOF
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
main() {
|
|
716
|
+
local subcommand="${1:-help}"
|
|
717
|
+
shift || true
|
|
718
|
+
|
|
719
|
+
case "$subcommand" in
|
|
720
|
+
capture)
|
|
721
|
+
cmd_capture "$@"
|
|
722
|
+
;;
|
|
723
|
+
verify)
|
|
724
|
+
cmd_verify "$@"
|
|
725
|
+
;;
|
|
726
|
+
pre-pr)
|
|
727
|
+
cmd_pre_pr "$@"
|
|
728
|
+
;;
|
|
729
|
+
status)
|
|
730
|
+
cmd_status
|
|
731
|
+
;;
|
|
732
|
+
types)
|
|
733
|
+
cmd_list_types
|
|
734
|
+
;;
|
|
735
|
+
help|--help|-h)
|
|
736
|
+
show_help
|
|
737
|
+
;;
|
|
738
|
+
*)
|
|
739
|
+
error "Unknown subcommand: $subcommand"
|
|
740
|
+
show_help
|
|
741
|
+
return 1
|
|
742
|
+
;;
|
|
743
|
+
esac
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
747
|
+
main "$@"
|
|
748
|
+
fi
|