bigpowers 2.86.1 → 2.86.3
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/.agents/skills/security-review/SKILL.md +3 -1
- package/.agents/skills/smoke-test/SKILL.md +1 -1
- package/.agents/skills/wire-ci/SKILL.md +78 -59
- package/.codebuddy/skills/security-review/SKILL.md +3 -1
- package/.codebuddy/skills/smoke-test/SKILL.md +1 -1
- package/.codebuddy/skills/wire-ci/SKILL.md +78 -59
- package/.codex/skills/security-review/SKILL.md +3 -1
- package/.codex/skills/smoke-test/SKILL.md +1 -1
- package/.codex/skills/wire-ci/SKILL.md +78 -59
- package/.hermes/skills/security-review/SKILL.md +3 -1
- package/.hermes/skills/smoke-test/SKILL.md +1 -1
- package/.hermes/skills/wire-ci/SKILL.md +78 -59
- package/.kilocode/rules/security-review.md +3 -1
- package/.kilocode/rules/smoke-test.md +1 -1
- package/.kilocode/rules/wire-ci.md +78 -59
- package/.mimocode/skills/security-review/SKILL.md +3 -1
- package/.mimocode/skills/smoke-test/SKILL.md +1 -1
- package/.mimocode/skills/wire-ci/SKILL.md +78 -59
- package/.pi/package.json +1 -1
- package/.pi/prompts/security-review.md +3 -1
- package/.pi/prompts/smoke-test.md +1 -1
- package/.pi/prompts/wire-ci.md +78 -59
- package/.pi/skills/security-review/SKILL.md +3 -1
- package/.pi/skills/smoke-test/SKILL.md +1 -1
- package/.pi/skills/wire-ci/SKILL.md +78 -59
- package/.trae/skills/security-review/SKILL.md +3 -1
- package/.trae/skills/smoke-test/SKILL.md +1 -1
- package/.trae/skills/wire-ci/SKILL.md +78 -59
- package/.windsurf/rules/security-review.md +3 -1
- package/.windsurf/rules/smoke-test.md +1 -1
- package/.windsurf/rules/wire-ci.md +78 -59
- package/.zcode/skills/security-review/SKILL.md +3 -1
- package/.zcode/skills/smoke-test/SKILL.md +1 -1
- package/.zcode/skills/wire-ci/SKILL.md +78 -59
- package/CHANGELOG.md +21 -0
- package/SKILL-INDEX.md +1 -1
- package/package.json +1 -1
- package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-312.pyc +0 -0
- package/scripts/lib/context-wire.sh +9 -0
- package/scripts/lib/detect-forge.sh +105 -0
- package/scripts/lib/extract-story-verify.py +102 -0
- package/scripts/lib/fail-open-detect.sh +67 -0
- package/scripts/lib/golden-suite-gates.sh +4 -0
- package/scripts/lib/plan-consistency-check.sh +26 -2
- package/scripts/lib/test-assertions.sh +11 -1
- package/scripts/run-skill-verify.sh +5 -46
- package/scripts/run-story-verify.sh +208 -0
- package/scripts/test-adapters.sh +14 -8
- package/scripts/verify-cwe-fixture-sync.sh +88 -0
- package/scripts/verify-install.sh +13 -1
- package/scripts/wire-ci.sh +159 -0
- package/skills/security-review/SKILL.md +3 -1
- package/skills/security-review/fixtures/CWE-639-idor-negative.go +48 -0
- package/skills/security-review/fixtures/CWE-639-idor-positive.go +29 -0
- package/skills/smoke-test/SKILL.md +1 -1
- package/skills/wire-ci/REFERENCE.md +15 -4
- package/skills/wire-ci/SKILL.md +63 -55
- package/skills-lock.json +4 -4
|
@@ -25,47 +25,11 @@ SKILLS_ROOT="$REPO_ROOT"
|
|
|
25
25
|
PASS=0; FAIL=0; SKIP=0
|
|
26
26
|
TARGET="${1:-}"
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
# (head/wc/cat/tee/sort/tr/sed/awk/echo/true/:). `grep x | wc -l` is the
|
|
34
|
-
# canonical offender: wc always succeeds, so the grep result is discarded.
|
|
35
|
-
# Use an assertion instead, e.g. [ "$(... | wc -l)" -gt 0 ].
|
|
36
|
-
FAIL_OPEN_TAIL_CMDS='head|wc|cat|tee|sort|tr|sed|awk|echo|true|:'
|
|
37
|
-
|
|
38
|
-
is_fail_open_directive() {
|
|
39
|
-
local cmd="$1"
|
|
40
|
-
|
|
41
|
-
# Family 1: explicit exit-status swallowing.
|
|
42
|
-
if echo "$cmd" | grep -qE '\|\|[[:space:]]*(echo|true|:)|;[[:space:]]*(true|:|exit[[:space:]]+0)[[:space:]]*$'; then
|
|
43
|
-
return 0
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
# Family 2: pipeline whose last stage never fails.
|
|
47
|
-
# Strip $(...) first — a pipe inside a command substitution feeds an
|
|
48
|
-
# assertion, e.g. [ "$(ls x | wc -l)" -gt 0 ], and is NOT fail-open.
|
|
49
|
-
local stripped="$cmd" prev=""
|
|
50
|
-
while [ "$stripped" != "$prev" ]; do
|
|
51
|
-
prev="$stripped"
|
|
52
|
-
stripped=$(printf '%s' "$stripped" | sed 's/\$([^()]*)/SUBST/g')
|
|
53
|
-
done
|
|
54
|
-
|
|
55
|
-
# Only inspect the segment after the final `|` that is not part of `||`.
|
|
56
|
-
local tail_seg
|
|
57
|
-
tail_seg=$(printf '%s' "$stripped" | sed 's/||/\
|
|
58
|
-
/g' | tail -1)
|
|
59
|
-
case "$tail_seg" in
|
|
60
|
-
*\|*)
|
|
61
|
-
tail_seg=${tail_seg##*|}
|
|
62
|
-
tail_seg=$(printf '%s' "$tail_seg" | sed 's/^[[:space:]]*//')
|
|
63
|
-
echo "$tail_seg" | grep -qE "^($FAIL_OPEN_TAIL_CMDS)([[:space:]]|$)" && return 0
|
|
64
|
-
;;
|
|
65
|
-
esac
|
|
66
|
-
|
|
67
|
-
return 1
|
|
68
|
-
}
|
|
28
|
+
# is_fail_open_directive / is_executable_verify now live in the shared detector,
|
|
29
|
+
# so tier-1 (here) and tier-2 (run-story-verify.sh, #106) reject the same
|
|
30
|
+
# idioms. A new evasion must be closed in one place, not two.
|
|
31
|
+
# shellcheck source=lib/fail-open-detect.sh
|
|
32
|
+
source "$REPO_ROOT/scripts/lib/fail-open-detect.sh"
|
|
69
33
|
|
|
70
34
|
normalize_verify_cmd() {
|
|
71
35
|
local raw="$1"
|
|
@@ -74,11 +38,6 @@ normalize_verify_cmd() {
|
|
|
74
38
|
echo "$raw"
|
|
75
39
|
}
|
|
76
40
|
|
|
77
|
-
is_executable_verify() {
|
|
78
|
-
local cmd="$1"
|
|
79
|
-
echo "$cmd" | grep -qE '^[a-zA-Z][a-zA-Z0-9_.-]*\b|^\['
|
|
80
|
-
}
|
|
81
|
-
|
|
82
41
|
run_verify_cmd() {
|
|
83
42
|
local skill="$1"
|
|
84
43
|
local cmd="$2"
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: BUG-2026-07-26-story-verify-never-executed
|
|
3
|
+
# Execute story-level verify: commands declared in specs/epics/*/epic.yaml.
|
|
4
|
+
# Tier 2 of the verify arc (GH #106). Tier 1 is run-skill-verify.sh.
|
|
5
|
+
#
|
|
6
|
+
# Only stories marked `status: done` are gated: a done story asserts its own
|
|
7
|
+
# acceptance criteria hold. Backlog/in-progress stories are reported as SKIP.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash scripts/run-story-verify.sh # gate all done stories
|
|
11
|
+
# bash scripts/run-story-verify.sh e80s05 # one story
|
|
12
|
+
# bash scripts/run-story-verify.sh --self-test # prove the gate can fail
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
17
|
+
cd "$REPO_ROOT"
|
|
18
|
+
|
|
19
|
+
# shellcheck source=lib/fail-open-detect.sh
|
|
20
|
+
source "$REPO_ROOT/scripts/lib/fail-open-detect.sh"
|
|
21
|
+
# shellcheck source=lib/python-env.sh
|
|
22
|
+
source "$REPO_ROOT/scripts/lib/python-env.sh"
|
|
23
|
+
resolve_python || exit 1
|
|
24
|
+
|
|
25
|
+
VERIFY_TIMEOUT_SECONDS=120
|
|
26
|
+
|
|
27
|
+
# Portable timeout: macOS lacks GNU timeout; use perl alarm as fallback.
|
|
28
|
+
# stdin is closed for every verify command: gate_all reads its record list on
|
|
29
|
+
# fd 0, and a child that reads stdin (any test-*.sh sourcing a read loop) would
|
|
30
|
+
# otherwise swallow the remaining records and silently shrink the gate.
|
|
31
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
32
|
+
run_with_timeout() { timeout "$VERIFY_TIMEOUT_SECONDS" bash -c "$1" </dev/null 2>&1; }
|
|
33
|
+
elif command -v perl >/dev/null 2>&1; then
|
|
34
|
+
run_with_timeout() { perl -e "alarm $VERIFY_TIMEOUT_SECONDS; exec @ARGV" bash -c "$1" </dev/null 2>&1; }
|
|
35
|
+
else
|
|
36
|
+
run_with_timeout() { bash -c "$1" </dev/null 2>&1; }
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
GATED_STATUS="done"
|
|
40
|
+
PASS=0; FAIL=0; SKIP=0
|
|
41
|
+
TARGET="${1:-}"
|
|
42
|
+
|
|
43
|
+
extract_records() {
|
|
44
|
+
"$PYTHON" "$REPO_ROOT/scripts/lib/extract-story-verify.py" "$REPO_ROOT"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Report a story whose directive can never fail. Kept separate from execution so
|
|
48
|
+
# a fail-open directive is a FAIL even when the command happens to exit 0.
|
|
49
|
+
reject_fail_open() {
|
|
50
|
+
local sid="$1" src="$2" cmd="$3"
|
|
51
|
+
echo "FAIL: $sid [$src] — fail-open directive (cannot exit non-zero): $cmd"
|
|
52
|
+
FAIL=$((FAIL + 1))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
reject_non_executable() {
|
|
56
|
+
local sid="$1" src="$2" cmd="$3"
|
|
57
|
+
echo "FAIL: $sid [$src] — verify is prose, not a runnable command: $cmd"
|
|
58
|
+
FAIL=$((FAIL + 1))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
run_story_verify() {
|
|
62
|
+
local sid="$1" src="$2" cmd="$3"
|
|
63
|
+
|
|
64
|
+
if ! is_executable_verify "$cmd"; then
|
|
65
|
+
reject_non_executable "$sid" "$src" "$cmd"
|
|
66
|
+
return 1
|
|
67
|
+
fi
|
|
68
|
+
if is_fail_open_directive "$cmd"; then
|
|
69
|
+
reject_fail_open "$sid" "$src" "$cmd"
|
|
70
|
+
return 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
local output
|
|
74
|
+
if output=$(run_with_timeout "$cmd"); then
|
|
75
|
+
echo "PASS: $sid"
|
|
76
|
+
PASS=$((PASS + 1))
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
echo "FAIL: $sid [$src] — $cmd"
|
|
80
|
+
echo " output: $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
81
|
+
FAIL=$((FAIL + 1))
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
gate_all() {
|
|
86
|
+
local sid src status cmd
|
|
87
|
+
while IFS=$'\t' read -r sid src status cmd; do
|
|
88
|
+
[ -z "${sid:-}" ] && continue
|
|
89
|
+
if [ -n "$TARGET" ] && [ "$sid" != "$TARGET" ]; then
|
|
90
|
+
continue
|
|
91
|
+
fi
|
|
92
|
+
if [ -z "$TARGET" ] && [ "$status" != "$GATED_STATUS" ]; then
|
|
93
|
+
echo "SKIP: $sid (status: $status)"
|
|
94
|
+
SKIP=$((SKIP + 1))
|
|
95
|
+
continue
|
|
96
|
+
fi
|
|
97
|
+
run_story_verify "$sid" "$src" "$cmd" || true
|
|
98
|
+
done < <(extract_records)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Anti-vacuity proof: seed a capsule whose done story must fail, and one that
|
|
102
|
+
# must pass, then require the runner to agree. A gate that cannot fail is the
|
|
103
|
+
# defect this script exists to close (#96/#106) — so it must prove otherwise.
|
|
104
|
+
run_self_test() {
|
|
105
|
+
echo "=== run-story-verify self-test ==="
|
|
106
|
+
local tmp_root
|
|
107
|
+
tmp_root=$(mktemp -d)
|
|
108
|
+
trap 'rm -rf "$tmp_root"' EXIT
|
|
109
|
+
|
|
110
|
+
mkdir -p "$tmp_root/specs/epics/e99-selftest"
|
|
111
|
+
cat > "$tmp_root/specs/epics/e99-selftest/epic.yaml" <<'YAML'
|
|
112
|
+
id: e99
|
|
113
|
+
title: self-test capsule
|
|
114
|
+
stories:
|
|
115
|
+
- id: e99s01
|
|
116
|
+
title: passing story
|
|
117
|
+
status: done
|
|
118
|
+
verify: test -d specs
|
|
119
|
+
- id: e99s02
|
|
120
|
+
title: failing story
|
|
121
|
+
status: done
|
|
122
|
+
verify: test -f /nonexistent/path/for/story-verify-self-test
|
|
123
|
+
- id: e99s03
|
|
124
|
+
title: fail-open story
|
|
125
|
+
status: done
|
|
126
|
+
verify: test -f /nonexistent || true
|
|
127
|
+
- id: e99s04
|
|
128
|
+
title: prose story
|
|
129
|
+
status: done
|
|
130
|
+
verify: node bin/bigpowers.js setup (interactive in TTY)
|
|
131
|
+
- id: e99s05
|
|
132
|
+
title: backlog story is not gated
|
|
133
|
+
status: backlog
|
|
134
|
+
verify: test -f /nonexistent/should/not/run
|
|
135
|
+
YAML
|
|
136
|
+
|
|
137
|
+
local records
|
|
138
|
+
records=$("$PYTHON" "$REPO_ROOT/scripts/lib/extract-story-verify.py" "$tmp_root")
|
|
139
|
+
|
|
140
|
+
local checked=0 problems=0
|
|
141
|
+
local sid src status cmd
|
|
142
|
+
while IFS=$'\t' read -r sid src status cmd; do
|
|
143
|
+
[ -z "${sid:-}" ] && continue
|
|
144
|
+
checked=$((checked + 1))
|
|
145
|
+
case "$sid" in
|
|
146
|
+
e99s01)
|
|
147
|
+
if is_fail_open_directive "$cmd" || ! is_executable_verify "$cmd"; then
|
|
148
|
+
echo "FAIL: self-test — genuine assertion rejected: $cmd"; problems=$((problems + 1))
|
|
149
|
+
elif ! run_with_timeout "$cmd" >/dev/null; then
|
|
150
|
+
echo "FAIL: self-test — passing fixture did not pass"; problems=$((problems + 1))
|
|
151
|
+
fi
|
|
152
|
+
;;
|
|
153
|
+
e99s02)
|
|
154
|
+
if run_with_timeout "$cmd" >/dev/null; then
|
|
155
|
+
echo "FAIL: self-test — failing fixture exited 0 (gate cannot fail)"; problems=$((problems + 1))
|
|
156
|
+
fi
|
|
157
|
+
;;
|
|
158
|
+
e99s03)
|
|
159
|
+
if ! is_fail_open_directive "$cmd"; then
|
|
160
|
+
echo "FAIL: self-test — fail-open directive not detected: $cmd"; problems=$((problems + 1))
|
|
161
|
+
fi
|
|
162
|
+
;;
|
|
163
|
+
e99s04)
|
|
164
|
+
if is_executable_verify "$cmd"; then
|
|
165
|
+
echo "FAIL: self-test — prose accepted as a command: $cmd"; problems=$((problems + 1))
|
|
166
|
+
fi
|
|
167
|
+
;;
|
|
168
|
+
e99s05)
|
|
169
|
+
if [ "$status" = "$GATED_STATUS" ]; then
|
|
170
|
+
echo "FAIL: self-test — backlog story reported as done"; problems=$((problems + 1))
|
|
171
|
+
fi
|
|
172
|
+
;;
|
|
173
|
+
esac
|
|
174
|
+
done <<EOF
|
|
175
|
+
$records
|
|
176
|
+
EOF
|
|
177
|
+
|
|
178
|
+
rm -rf "$tmp_root"
|
|
179
|
+
trap - EXIT
|
|
180
|
+
|
|
181
|
+
if [ "$checked" -lt 5 ]; then
|
|
182
|
+
echo "FAIL: self-test — extractor found $checked/5 fixture stories"
|
|
183
|
+
return 1
|
|
184
|
+
fi
|
|
185
|
+
if [ "$problems" -gt 0 ]; then
|
|
186
|
+
echo "FAIL: self-test — $problems assertion(s) failed"
|
|
187
|
+
return 1
|
|
188
|
+
fi
|
|
189
|
+
echo "PASS: self-test — gate fails on broken stories, passes on sound ones"
|
|
190
|
+
return 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if [ "$TARGET" = "--self-test" ]; then
|
|
194
|
+
run_self_test
|
|
195
|
+
exit $?
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
gate_all
|
|
199
|
+
|
|
200
|
+
echo ""
|
|
201
|
+
echo "Results: $PASS PASS, $FAIL FAIL, $SKIP SKIP"
|
|
202
|
+
|
|
203
|
+
if [ -n "$TARGET" ] && [ "$((PASS + FAIL))" -eq 0 ]; then
|
|
204
|
+
echo "ERROR: no story matching '$TARGET' declares a verify: command" >&2
|
|
205
|
+
exit 2
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
[ "$FAIL" -eq 0 ]
|
package/scripts/test-adapters.sh
CHANGED
|
@@ -21,7 +21,6 @@ bash "$SCRIPT_DIR/validate-targets-yaml.sh" >/dev/null
|
|
|
21
21
|
|
|
22
22
|
PASS=0
|
|
23
23
|
FAIL=0
|
|
24
|
-
TMPDIR=""
|
|
25
24
|
TA_PASS=0
|
|
26
25
|
TA_FAIL=0
|
|
27
26
|
TA_TMPDIR=""
|
|
@@ -56,15 +55,22 @@ test_adapter() {
|
|
|
56
55
|
if [[ "$has_context" == "true" && "$mode" != "native" ]]; then
|
|
57
56
|
declare -f wire_context >/dev/null 2>&1 && ta_pass "$id: wire_context defined" || ta_fail "$id: wire_context missing"
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
cp "$SCRIPT_DIR/../docs/templates/AGENTS.md" "$
|
|
61
|
-
(
|
|
62
|
-
|
|
58
|
+
TA_TMPDIR=$(mktemp -d)
|
|
59
|
+
cp "$SCRIPT_DIR/../docs/templates/AGENTS.md" "$TA_TMPDIR/AGENTS.md" 2>/dev/null || echo "# test" > "$TA_TMPDIR/AGENTS.md"
|
|
60
|
+
# The idempotency probe runs in a subshell (it cd's away), so its ta_pass/
|
|
61
|
+
# ta_fail would increment counters in a child and be lost — the script used
|
|
62
|
+
# to print 5 PASS lines and report 4. Capture the status, tally in-process.
|
|
63
|
+
if (
|
|
64
|
+
cd "$TA_TMPDIR"
|
|
63
65
|
# shellcheck source=/dev/null
|
|
64
66
|
source "$adapter_file"
|
|
65
|
-
wire_context
|
|
66
|
-
wire_context
|
|
67
|
-
)
|
|
67
|
+
wire_context >/dev/null 2>&1 || true
|
|
68
|
+
wire_context >/dev/null 2>&1
|
|
69
|
+
); then
|
|
70
|
+
ta_pass "$id: idempotent wire_context"
|
|
71
|
+
else
|
|
72
|
+
ta_fail "$id: wire_context failed"
|
|
73
|
+
fi
|
|
68
74
|
fi
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e80s05
|
|
3
|
+
# Assert security-review's CWE rule table and its fixtures/ directory agree.
|
|
4
|
+
#
|
|
5
|
+
# e80s05 ("security-review fixture table growth") was marked done while the
|
|
6
|
+
# fixtures directory gained a class the table never listed. Remembering to
|
|
7
|
+
# update both is exactly the kind of discipline that decays, so this makes the
|
|
8
|
+
# drift mechanical instead: every fixture class needs a table row, and every
|
|
9
|
+
# table row needs both fixtures on disk.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# bash scripts/verify-cwe-fixture-sync.sh [--self-test]
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
16
|
+
SKILL_MD="$REPO_ROOT/skills/security-review/SKILL.md"
|
|
17
|
+
FIXTURES="$REPO_ROOT/skills/security-review/fixtures"
|
|
18
|
+
|
|
19
|
+
fail=0
|
|
20
|
+
note() { echo " $1"; }
|
|
21
|
+
|
|
22
|
+
check_sync() {
|
|
23
|
+
local skill_md="$1" fixtures="$2"
|
|
24
|
+
local rc=0
|
|
25
|
+
|
|
26
|
+
[ -f "$skill_md" ] || { echo "FAIL: missing $skill_md"; return 1; }
|
|
27
|
+
[ -d "$fixtures" ] || { echo "FAIL: missing $fixtures"; return 1; }
|
|
28
|
+
|
|
29
|
+
# Classes present on disk, derived from the -positive fixture filenames.
|
|
30
|
+
local disk_classes
|
|
31
|
+
disk_classes=$(find "$fixtures" -name '*-positive.*' -exec basename {} \; 2>/dev/null \
|
|
32
|
+
| sed 's/-positive\..*$//' | sort -u)
|
|
33
|
+
|
|
34
|
+
# Every disk class must appear in the rule table.
|
|
35
|
+
local c
|
|
36
|
+
for c in $disk_classes; do
|
|
37
|
+
if ! grep -q "$c" "$skill_md"; then
|
|
38
|
+
note "FAIL: fixture class '$c' has no row in the CWE rule table"
|
|
39
|
+
rc=1
|
|
40
|
+
fi
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
# Every fixture referenced by the table must exist, both polarities.
|
|
44
|
+
local ref
|
|
45
|
+
for ref in $(grep -oE 'fixtures/[A-Za-z0-9._-]+' "$skill_md" | sed 's|fixtures/||' | sort -u); do
|
|
46
|
+
if [ ! -f "$fixtures/$ref" ]; then
|
|
47
|
+
note "FAIL: table references fixtures/$ref which does not exist"
|
|
48
|
+
rc=1
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
for c in $disk_classes; do
|
|
53
|
+
local pos neg
|
|
54
|
+
pos=$(find "$fixtures" -name "$c-positive.*" | head -1)
|
|
55
|
+
neg=$(find "$fixtures" -name "$c-negative.*" | head -1)
|
|
56
|
+
if [ -z "$pos" ] || [ -z "$neg" ]; then
|
|
57
|
+
note "FAIL: class '$c' needs BOTH a -positive and a -negative fixture"
|
|
58
|
+
rc=1
|
|
59
|
+
fi
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
return $rc
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# --self-test proves this gate can fail: point it at a fixtures dir holding a
|
|
66
|
+
# class the table does not list, and require a non-zero exit.
|
|
67
|
+
if [ "${1:-}" = "--self-test" ]; then
|
|
68
|
+
tmp=$(mktemp -d)
|
|
69
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
70
|
+
mkdir -p "$tmp/fixtures"
|
|
71
|
+
printf '| Rule | CWE | Positive fixture | Negative fixture |\n' > "$tmp/SKILL.md"
|
|
72
|
+
: > "$tmp/fixtures/CWE-999-unlisted-positive.py"
|
|
73
|
+
: > "$tmp/fixtures/CWE-999-unlisted-negative.py"
|
|
74
|
+
|
|
75
|
+
if check_sync "$tmp/SKILL.md" "$tmp/fixtures" >/dev/null 2>&1; then
|
|
76
|
+
echo "FAIL: self-test — gate passed on an unlisted fixture class (it cannot fail)"
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
echo "PASS: self-test — gate rejects a fixture class missing from the table"
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
if check_sync "$SKILL_MD" "$FIXTURES"; then
|
|
84
|
+
echo "PASS: CWE rule table and fixtures/ are in sync"
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
echo "FAIL: security-review CWE table and fixtures/ have drifted"
|
|
88
|
+
exit 1
|
|
@@ -168,7 +168,19 @@ grep -q "'antigravity'" "$REPO_ROOT/bin/setup.js" && grep -q 'SUPPORTED_IDS' "$R
|
|
|
168
168
|
grep -q "'agy'" "$REPO_ROOT/bin/setup.js" && ta_pass "setup.js: agy supported" || ta_fail "setup.js: agy not in SUPPORTED_IDS"
|
|
169
169
|
grep -q "case 'antigravity'" "$REPO_ROOT/scripts/lib/install-helpers.js" && ta_pass "install-helpers: antigravity case" || ta_fail "install-helpers: missing antigravity case"
|
|
170
170
|
grep -q "case 'agy'" "$REPO_ROOT/scripts/lib/install-helpers.js" && ta_pass "install-helpers: agy case" || ta_fail "install-helpers: missing agy case"
|
|
171
|
-
|
|
171
|
+
# Scope this to install_agy's OWN body. The previous form piped a herestring into
|
|
172
|
+
# install_grep(), which is `grep "$@" $(install_grep_paths)` — it always greps the
|
|
173
|
+
# install files and ignores stdin, so the assertion actually searched every target.
|
|
174
|
+
# install_gemini/install_claude legitimately use .gemini/extensions/bigpowers
|
|
175
|
+
# (install-targets-a.sh:21,22,70), so the check could never pass. (#106)
|
|
176
|
+
AGY_BODY="$(sed -n '/^install_agy()/,/^}/p' $(install_grep_paths) 2>/dev/null)"
|
|
177
|
+
if [[ -z "$AGY_BODY" ]]; then
|
|
178
|
+
ta_fail "source: install_agy() body not found — assertion would pass vacuously"
|
|
179
|
+
elif printf '%s' "$AGY_BODY" | grep -q '\.gemini/extensions/bigpowers'; then
|
|
180
|
+
ta_fail "source: agy touches gemini extension path"
|
|
181
|
+
else
|
|
182
|
+
ta_pass "source: agy avoids gemini extension path"
|
|
183
|
+
fi
|
|
172
184
|
|
|
173
185
|
install_grep -q 'install_codex()' && ta_pass "source: install_codex()" || ta_fail "source: missing install_codex()"
|
|
174
186
|
install_grep -q 'uninstall_codex()' && ta_pass "source: uninstall_codex()" || ta_fail "source: missing uninstall_codex()"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: GH-104-forge-neutrality
|
|
3
|
+
# wire-ci.sh — resolve forge + stack and place a CI template, or skip honestly.
|
|
4
|
+
#
|
|
5
|
+
# GH #104: wire-ci declared itself a HARD GATE, then generated GitHub Actions
|
|
6
|
+
# config copied from one person's personal repository. On GitLab it could not
|
|
7
|
+
# work at all. This runner makes the template source configurable and turns an
|
|
8
|
+
# unsupported forge into an explicit skip instead of a false guarantee.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# bash scripts/wire-ci.sh --detect # report forge + stack, write nothing
|
|
12
|
+
# bash scripts/wire-ci.sh --plan # show what would be written
|
|
13
|
+
# bash scripts/wire-ci.sh --apply # write the workflow
|
|
14
|
+
# bash scripts/wire-ci.sh --self-test # prove skip/apply paths behave
|
|
15
|
+
#
|
|
16
|
+
# Exit codes: 0 applied or cleanly skipped, 1 error, 3 unsupported forge.
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
21
|
+
# shellcheck source=lib/detect-forge.sh
|
|
22
|
+
source "$REPO_ROOT/scripts/lib/detect-forge.sh"
|
|
23
|
+
|
|
24
|
+
EXIT_UNSUPPORTED_FORGE=3
|
|
25
|
+
MODE="${1:---detect}"
|
|
26
|
+
TARGET_DIR="${WIRE_CI_TARGET:-$REPO_ROOT}"
|
|
27
|
+
|
|
28
|
+
detect_stack() {
|
|
29
|
+
local root="$1"
|
|
30
|
+
if [[ -f "$root/Cargo.toml" ]]; then echo "rust"
|
|
31
|
+
elif [[ -f "$root/go.mod" ]]; then echo "go"
|
|
32
|
+
elif [[ -f "$root/pyproject.toml" || -f "$root/setup.py" ]]; then echo "python"
|
|
33
|
+
elif [[ -f "$root/package.json" ]]; then echo "node"
|
|
34
|
+
else echo "unknown"
|
|
35
|
+
fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
template_for() {
|
|
39
|
+
local forge="$1" stack="$2"
|
|
40
|
+
printf '%s/%s/test-build-release-%s.yml' "$(forge_template_root)" "$forge" "$stack"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# The honest-degradation path. An unsupported forge gets a named reason and a
|
|
44
|
+
# non-zero-but-distinct exit code, never a GitHub workflow it cannot run.
|
|
45
|
+
report_unsupported() {
|
|
46
|
+
local forge="$1"
|
|
47
|
+
echo "wire-ci: SKIP — no CI templates for forge '$forge'"
|
|
48
|
+
echo " Supported: $FORGE_SUPPORTED_LIST"
|
|
49
|
+
echo " wire-ci is NOT a hard gate on this forge; it cannot generate config it does not ship."
|
|
50
|
+
echo " Options:"
|
|
51
|
+
echo " - point at your own templates: BIGPOWERS_CI_TEMPLATES=/path/to/templates"
|
|
52
|
+
echo " - pin the forge explicitly: echo 'forge: github' > specs/forge.yaml"
|
|
53
|
+
echo " - write CI by hand for $(forge_ci_path "$forge" || true)"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
resolve_and_report() {
|
|
57
|
+
local root="$1"
|
|
58
|
+
FORGE=$(detect_forge "$root")
|
|
59
|
+
STACK=$(detect_stack "$root")
|
|
60
|
+
echo "wire-ci: forge=$FORGE stack=$STACK"
|
|
61
|
+
|
|
62
|
+
if ! forge_is_supported "$FORGE"; then
|
|
63
|
+
report_unsupported "$FORGE"
|
|
64
|
+
return "$EXIT_UNSUPPORTED_FORGE"
|
|
65
|
+
fi
|
|
66
|
+
if [[ "$STACK" == "unknown" ]]; then
|
|
67
|
+
echo "wire-ci: SKIP — no recognized manifest (Cargo.toml, go.mod, pyproject.toml, package.json)"
|
|
68
|
+
return "$EXIT_UNSUPPORTED_FORGE"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
TEMPLATE=$(template_for "$FORGE" "$STACK")
|
|
72
|
+
if [[ ! -f "$TEMPLATE" ]]; then
|
|
73
|
+
echo "wire-ci: SKIP — no bundled template at $TEMPLATE"
|
|
74
|
+
return "$EXIT_UNSUPPORTED_FORGE"
|
|
75
|
+
fi
|
|
76
|
+
echo "wire-ci: template=$TEMPLATE"
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
apply_template() {
|
|
81
|
+
local root="$1"
|
|
82
|
+
local dest_dir="$root/$(forge_ci_path "$FORGE")"
|
|
83
|
+
mkdir -p "$dest_dir"
|
|
84
|
+
cp "$TEMPLATE" "$dest_dir/test-build-release.yml"
|
|
85
|
+
echo "wire-ci: wrote $dest_dir/test-build-release.yml"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
run_self_test() {
|
|
89
|
+
echo "=== wire-ci self-test ==="
|
|
90
|
+
local tmp problems=0
|
|
91
|
+
tmp=$(mktemp -d)
|
|
92
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
93
|
+
|
|
94
|
+
# An unsupported forge must skip, not emit GitHub Actions config.
|
|
95
|
+
mkdir -p "$tmp/gitlab-project"
|
|
96
|
+
echo '{}' > "$tmp/gitlab-project/package.json"
|
|
97
|
+
echo 'forge: gitlab' > /dev/null
|
|
98
|
+
mkdir -p "$tmp/gitlab-project/specs"
|
|
99
|
+
printf 'forge: gitlab\n' > "$tmp/gitlab-project/specs/forge.yaml"
|
|
100
|
+
local out rc
|
|
101
|
+
out=$(BIGPOWERS_FORGE="" resolve_and_report "$tmp/gitlab-project" 2>&1); rc=$?
|
|
102
|
+
if [[ "$rc" -ne "$EXIT_UNSUPPORTED_FORGE" ]]; then
|
|
103
|
+
echo "FAIL: self-test — gitlab project did not skip (rc=$rc)"; problems=$((problems + 1))
|
|
104
|
+
fi
|
|
105
|
+
if printf '%s' "$out" | grep -q 'HARD GATE'; then
|
|
106
|
+
echo "FAIL: self-test — skip path still claims HARD GATE"; problems=$((problems + 1))
|
|
107
|
+
fi
|
|
108
|
+
if [[ -d "$tmp/gitlab-project/.github" ]]; then
|
|
109
|
+
echo "FAIL: self-test — wrote .github/ for a non-GitHub forge"; problems=$((problems + 1))
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# A supported forge must resolve to a bundled template — proving the runner
|
|
113
|
+
# does not depend on any external repository being reachable.
|
|
114
|
+
mkdir -p "$tmp/gh-project/specs"
|
|
115
|
+
echo '{}' > "$tmp/gh-project/package.json"
|
|
116
|
+
printf 'forge: github\n' > "$tmp/gh-project/specs/forge.yaml"
|
|
117
|
+
if ! out=$(resolve_and_report "$tmp/gh-project" 2>&1); then
|
|
118
|
+
echo "FAIL: self-test — github/node project did not resolve: $out"; problems=$((problems + 1))
|
|
119
|
+
elif ! printf '%s' "$out" | grep -q 'test-build-release-node.yml'; then
|
|
120
|
+
echo "FAIL: self-test — resolved the wrong template: $out"; problems=$((problems + 1))
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# The default template root must live inside the package, not in a user's home
|
|
124
|
+
# and not in the project being wired. Guards the #104 regression directly.
|
|
125
|
+
local root
|
|
126
|
+
root=$(forge_template_root)
|
|
127
|
+
case "$root" in
|
|
128
|
+
"$REPO_ROOT"/*) : ;;
|
|
129
|
+
*) echo "FAIL: self-test — default template root escapes the package: $root"; problems=$((problems + 1)) ;;
|
|
130
|
+
esac
|
|
131
|
+
if printf '%s' "$root" | grep -q 'danielvm-git\|~/Developer/.github'; then
|
|
132
|
+
echo "FAIL: self-test — template root still points at a personal repository"; problems=$((problems + 1))
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
rm -rf "$tmp"
|
|
136
|
+
trap - EXIT
|
|
137
|
+
|
|
138
|
+
if [[ "$problems" -gt 0 ]]; then
|
|
139
|
+
echo "FAIL: wire-ci self-test — $problems problem(s)"
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
142
|
+
echo "PASS: wire-ci self-test — unsupported forges skip, supported forges resolve bundled templates"
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case "$MODE" in
|
|
147
|
+
--self-test) run_self_test; exit $? ;;
|
|
148
|
+
--detect|--plan)
|
|
149
|
+
resolve_and_report "$TARGET_DIR"; exit $?
|
|
150
|
+
;;
|
|
151
|
+
--apply)
|
|
152
|
+
resolve_and_report "$TARGET_DIR" || exit $?
|
|
153
|
+
apply_template "$TARGET_DIR"
|
|
154
|
+
;;
|
|
155
|
+
*)
|
|
156
|
+
echo "wire-ci: unknown mode '$MODE' (expected --detect, --plan, --apply, --self-test)" >&2
|
|
157
|
+
exit 1
|
|
158
|
+
;;
|
|
159
|
+
esac
|
|
@@ -64,6 +64,8 @@ Every **new detection rule** added to this skill MUST:
|
|
|
64
64
|
|------|-----|------------------|------------------|
|
|
65
65
|
| SQL injection | CWE-89 | `fixtures/CWE-89-sqli-positive.py` | `fixtures/CWE-89-sqli-negative.py` |
|
|
66
66
|
| XSS (DOM) | CWE-79 | `fixtures/CWE-79-xss-positive.js` | `fixtures/CWE-79-xss-negative.js` |
|
|
67
|
+
| Missing tenant scoping (IDOR) | CWE-639 | `fixtures/CWE-639-idor-positive.go` | `fixtures/CWE-639-idor-negative.go` |
|
|
68
|
+
| Fail-open verify directive | CWE-754 | `fixtures/CWE-fail-open-verify-positive.sh` | `fixtures/CWE-fail-open-verify-negative.sh` |
|
|
67
69
|
|
|
68
70
|
Before merging a new category, run both fixtures through the detection guidance and confirm positive flags / negative passes.
|
|
69
71
|
|
|
@@ -115,4 +117,4 @@ Each finding: **`File:Line` — Severity — Category**
|
|
|
115
117
|
|
|
116
118
|
## Verify
|
|
117
119
|
|
|
118
|
-
→ verify: `test -d specs/security && test -f scripts/lib/parallel-review-worktrees.sh && git rev-parse HEAD >/dev/null 2>&1`
|
|
120
|
+
→ verify: `test -d specs/security && test -f scripts/lib/parallel-review-worktrees.sh && bash scripts/verify-cwe-fixture-sync.sh >/dev/null && git rev-parse HEAD >/dev/null 2>&1`
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// story: e80s05
|
|
2
|
+
// Negative fixture: correctly scoped tenant query (must NOT be flagged).
|
|
3
|
+
//
|
|
4
|
+
// The org comes from the authenticated context, never from the request, and it
|
|
5
|
+
// is part of the WHERE clause. A caller from another organization gets zero
|
|
6
|
+
// rows rather than someone else's site.
|
|
7
|
+
//
|
|
8
|
+
// Note the retrofit trap this deliberately avoids: bigbase's BUG-2026-07-24T184443
|
|
9
|
+
// added `org_id INTEGER NOT NULL DEFAULT 0`, which passed tests on new rows and
|
|
10
|
+
// hid 8 of 9 production sites whose legacy rows defaulted to org 0. Scope on a
|
|
11
|
+
// real, backfilled org_id — never a placeholder default.
|
|
12
|
+
package fixtures
|
|
13
|
+
|
|
14
|
+
import (
|
|
15
|
+
"database/sql"
|
|
16
|
+
"net/http"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
func orgIDFromContext(r *http.Request) (string, bool) {
|
|
20
|
+
v, ok := r.Context().Value(ctxOrgID).(string)
|
|
21
|
+
return v, ok && v != ""
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ctxKey string
|
|
25
|
+
|
|
26
|
+
const ctxOrgID ctxKey = "org_id"
|
|
27
|
+
|
|
28
|
+
func handleGetSiteNegative(w http.ResponseWriter, r *http.Request, db *sql.DB) {
|
|
29
|
+
orgID, ok := orgIDFromContext(r)
|
|
30
|
+
if !ok {
|
|
31
|
+
http.Error(w, "forbidden", http.StatusForbidden)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
siteID := r.URL.Query().Get("site_id")
|
|
35
|
+
|
|
36
|
+
// SAFE: the tenant predicate is not optional and does not come from input.
|
|
37
|
+
row := db.QueryRow(
|
|
38
|
+
`SELECT id, name, domain FROM sites WHERE id = ? AND org_id = ?`,
|
|
39
|
+
siteID, orgID,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
var id, name, domain string
|
|
43
|
+
if err := row.Scan(&id, &name, &domain); err != nil {
|
|
44
|
+
http.Error(w, "not found", http.StatusNotFound)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
_, _ = w.Write([]byte(name + " " + domain))
|
|
48
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// story: e80s05
|
|
2
|
+
// Positive fixture: IDOR / missing tenant scoping (must be flagged).
|
|
3
|
+
//
|
|
4
|
+
// Modelled on the bigbase family (BUG-129/130/133/135/136/140/143): the
|
|
5
|
+
// handler authenticates the caller, then trusts an ID from the request and
|
|
6
|
+
// queries by that ID alone. Authentication is proven; authorization is not.
|
|
7
|
+
// Nothing in the type system or a diff-scoped review makes this look wrong —
|
|
8
|
+
// the bug is a predicate that was never written.
|
|
9
|
+
package fixtures
|
|
10
|
+
|
|
11
|
+
import (
|
|
12
|
+
"database/sql"
|
|
13
|
+
"net/http"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func handleGetSitePositive(w http.ResponseWriter, r *http.Request, db *sql.DB) {
|
|
17
|
+
siteID := r.URL.Query().Get("site_id")
|
|
18
|
+
|
|
19
|
+
// VULNERABLE: scoped by primary key only. Any authenticated caller from any
|
|
20
|
+
// organization can read any site by guessing or enumerating site_id.
|
|
21
|
+
row := db.QueryRow(`SELECT id, name, domain FROM sites WHERE id = ?`, siteID)
|
|
22
|
+
|
|
23
|
+
var id, name, domain string
|
|
24
|
+
if err := row.Scan(&id, &name, &domain); err != nil {
|
|
25
|
+
http.Error(w, "not found", http.StatusNotFound)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
_, _ = w.Write([]byte(name + " " + domain))
|
|
29
|
+
}
|