feat(10.1-04): add the fail-closed Phase 10.1 gate
scripts/check-phase10.1.sh: --self-test, --go, --security, --postgres, --spa, --openapi, --dist, --hygiene, --evidence and --all. - phase101_detect refuses failed, skipped, zero-test, non-JSON and build-failed runs and required tests that did not pass - hygiene_101 refuses HTML-string parsers in admin/src, network, cookie or storage access in application plugin asset JS, and script, style or inline handler markup in application partial templates; each rule is proven by its own self-test plant - --evidence requires a review row and, for every high threat, a named test and a removal check row
This commit is contained in:
441
scripts/check-phase10.1.sh
Executable file
441
scripts/check-phase10.1.sh
Executable file
@@ -0,0 +1,441 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Phase 10.1 fail-closed gate (runtime admin extension point, ADMIN-07).
|
||||||
|
# Every stage exits non-zero on a failing command, a go test run that fails,
|
||||||
|
# skips or matches zero tests, a named test that did not pass, OpenAPI or
|
||||||
|
# dist drift, a hygiene violation or an evidence gap. --self-test proves each
|
||||||
|
# detector and each Phase 10.1 hygiene rule fails closed.
|
||||||
|
#
|
||||||
|
# Allow-list: none. The fonoteka.go parity failures TestMigrateSeedsCanonicalGenres
|
||||||
|
# and TestSchemaMatchesPHPSnapshot, accepted until then, pass since fonoteka.go
|
||||||
|
# 21c0f12 (fix(09): update parity expectations for the backend admin schema),
|
||||||
|
# and the detector refuses an allow-listed failure that passes. KNOWN_APP_FAILURES
|
||||||
|
# stays as the one place to name a future known failure, with a reason.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
APP="$(cd "$ROOT/../fonoteka.go" && pwd)"
|
||||||
|
PHASE_DIR="$ROOT/.planning/phases/10.1-runtime-admin-extension-point"
|
||||||
|
REVIEW="$PHASE_DIR/10.1-SECURITY-REVIEW.md"
|
||||||
|
VALIDATION="$PHASE_DIR/10.1-VALIDATION.md"
|
||||||
|
APP_PLUGINS=(./plugins/golem15/fonoteka/... ./plugins/golem15/user/...)
|
||||||
|
KNOWN_APP_FAILURES=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
usage:
|
||||||
|
check-phase10.1.sh --self-test
|
||||||
|
check-phase10.1.sh --go
|
||||||
|
check-phase10.1.sh --security
|
||||||
|
check-phase10.1.sh --postgres
|
||||||
|
check-phase10.1.sh --spa
|
||||||
|
check-phase10.1.sh --openapi
|
||||||
|
check-phase10.1.sh --dist
|
||||||
|
check-phase10.1.sh --hygiene
|
||||||
|
check-phase10.1.sh --evidence
|
||||||
|
check-phase10.1.sh --all
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# phase101_detect reads go test -json. Exit 1 fail, 2 skip, 3 zero tests,
|
||||||
|
# 4 non-JSON, 5 a required test did not pass, 6 an allow-listed failure now
|
||||||
|
# passes. PHASE101_REQUIRE lists test names that must pass; PHASE101_ALLOW
|
||||||
|
# lists "package:Test" failures that are accepted (and must still fail).
|
||||||
|
phase101_detect() {
|
||||||
|
python3 - "$1" <<'PY'
|
||||||
|
import json, os, sys
|
||||||
|
path = sys.argv[1]
|
||||||
|
allow = set(os.environ.get("PHASE101_ALLOW", "").split())
|
||||||
|
require = set(os.environ.get("PHASE101_REQUIRE", "").split())
|
||||||
|
passed = set()
|
||||||
|
failed_tests = {}
|
||||||
|
failed_pkgs = []
|
||||||
|
build_failed = False
|
||||||
|
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||||
|
for raw in fh:
|
||||||
|
line = raw.strip()
|
||||||
|
if not line.startswith("{"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ev = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("refuse: non-json test output", file=sys.stderr)
|
||||||
|
sys.exit(4)
|
||||||
|
action = ev.get("Action")
|
||||||
|
test = ev.get("Test") or ""
|
||||||
|
pkg = ev.get("Package") or ""
|
||||||
|
if action == "build-fail":
|
||||||
|
build_failed = True
|
||||||
|
if action == "skip" and test:
|
||||||
|
print(f"refuse: skipped {pkg} {test}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
if action == "fail":
|
||||||
|
if ev.get("FailedBuild"):
|
||||||
|
build_failed = True
|
||||||
|
if test:
|
||||||
|
failed_tests.setdefault(pkg, []).append(test)
|
||||||
|
else:
|
||||||
|
failed_pkgs.append(pkg)
|
||||||
|
if action == "pass" and test:
|
||||||
|
passed.add(test)
|
||||||
|
top = test.split("/", 1)[0]
|
||||||
|
if f"{pkg}:{top}" in allow and "/" not in test:
|
||||||
|
print(f"refuse: allow-listed failure {pkg} {test} now passes; remove it from the gate", file=sys.stderr)
|
||||||
|
sys.exit(6)
|
||||||
|
if build_failed:
|
||||||
|
print("refuse: build failed", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
accepted = []
|
||||||
|
for pkg, tests in failed_tests.items():
|
||||||
|
for test in tests:
|
||||||
|
top = test.split("/", 1)[0]
|
||||||
|
if f"{pkg}:{top}" in allow:
|
||||||
|
accepted.append(f"{pkg} {test}")
|
||||||
|
continue
|
||||||
|
print(f"refuse: failed {pkg} {test}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
for pkg in failed_pkgs:
|
||||||
|
if not failed_tests.get(pkg):
|
||||||
|
print(f"refuse: failed {pkg or 'unknown package'}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
for item in sorted(set(accepted)):
|
||||||
|
print(f"known pre-existing failure (deferred-items.md): {item}", file=sys.stderr)
|
||||||
|
missing = sorted(name for name in require if name not in passed)
|
||||||
|
if missing:
|
||||||
|
print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr)
|
||||||
|
sys.exit(5)
|
||||||
|
if not passed:
|
||||||
|
print("refuse: zero tests", file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
# phase101_go DIR PKGS... [-run REGEX] runs go test -json through the
|
||||||
|
# detector. The go test exit status is trusted only when no failure was
|
||||||
|
# allow-listed.
|
||||||
|
phase101_go() {
|
||||||
|
local dir="$1"
|
||||||
|
shift
|
||||||
|
local log err
|
||||||
|
log="$(mktemp)"
|
||||||
|
err="$(mktemp)"
|
||||||
|
set +e
|
||||||
|
(cd "$dir" && go test -json -count=1 "$@") >"$log" 2>"$err"
|
||||||
|
local rc=$?
|
||||||
|
set -e
|
||||||
|
local dc=0
|
||||||
|
phase101_detect "$log" || dc=$?
|
||||||
|
if [[ "$dc" -ne 0 || ("$rc" -ne 0 && -z "${PHASE101_ALLOW:-}") ]]; then
|
||||||
|
cat "$err" >&2 || true
|
||||||
|
tail -n 40 "$log" >&2 || true
|
||||||
|
rm -f "$log" "$err"
|
||||||
|
echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -f "$log" "$err"
|
||||||
|
}
|
||||||
|
|
||||||
|
# phase101_tests DIR PKG TEST... requires every named test to run and pass.
|
||||||
|
phase101_tests() {
|
||||||
|
local dir="$1" pkg="$2"
|
||||||
|
shift 2
|
||||||
|
local names="$*"
|
||||||
|
local regex="^($(tr ' ' '|' <<<"$names"))\$"
|
||||||
|
PHASE101_REQUIRE="$names" phase101_go "$dir" "$pkg" -run "$regex"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_detect() {
|
||||||
|
local name="$1" want="$2" payload="$3"
|
||||||
|
local log dc=0
|
||||||
|
log="$(mktemp)"
|
||||||
|
printf '%s\n' "$payload" >"$log"
|
||||||
|
phase101_detect "$log" 2>/dev/null || dc=$?
|
||||||
|
rm -f "$log"
|
||||||
|
if [[ "$dc" -ne "$want" ]]; then
|
||||||
|
echo "refuse: self-test $name: detector exit $dc, want $want" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# The hygiene_101 refusal reasons; the self-test checks each plant is refused
|
||||||
|
# for its own reason only.
|
||||||
|
REASON_PARSER="HTML-string parser in admin/src"
|
||||||
|
REASON_ASSET="network, cookie or storage access in application plugin asset JS"
|
||||||
|
REASON_PARTIAL="script or event-handler markup in an application partial template"
|
||||||
|
|
||||||
|
# hygiene_101 TREE APPTREE: the Phase 10.1 extension rules (D-04, D-05, D-17;
|
||||||
|
# T-10.1-08, T-10.1-10, T-10.1-14, T-10.1-20). The Phase 10 rules run first
|
||||||
|
# through check-phase10.sh --hygiene.
|
||||||
|
hygiene_101() {
|
||||||
|
local tree="$1" app="$2" bad=0 hits
|
||||||
|
fail101() {
|
||||||
|
echo "refuse: hygiene: $*" >&2
|
||||||
|
bad=1
|
||||||
|
}
|
||||||
|
# The SPA never parses a string as markup: partial nodes are built with h().
|
||||||
|
hits="$(cd "$tree" && grep -rnE 'setHTML(Unsafe)?\b|createContextualFragment|DOMParser|srcdoc|document\.write' admin/src 2>/dev/null || true)"
|
||||||
|
[[ -z "$hits" ]] || fail101 "$REASON_PARSER: $hits"
|
||||||
|
# Plugin elements signal through summer-action; the SPA owns HTTP and the
|
||||||
|
# session cookie is HttpOnly (D-05, D-08).
|
||||||
|
local js=()
|
||||||
|
if [[ -d "$app/plugins" ]]; then
|
||||||
|
mapfile -t js < <(find "$app/plugins" -mindepth 3 -path '*/assets/*' -type f \( -name '*.js' -o -name '*.mjs' \) | sort)
|
||||||
|
fi
|
||||||
|
if [[ "${#js[@]}" -gt 0 ]]; then
|
||||||
|
hits="$(grep -nHE '\bfetch[[:space:]]*\(|XMLHttpRequest|document\.cookie|localStorage|sessionStorage|indexedDB|sendBeacon|\bWebSocket\b|\bEventSource\b' "${js[@]}" || true)"
|
||||||
|
[[ -z "$hits" ]] || fail101 "$REASON_ASSET: $hits"
|
||||||
|
fi
|
||||||
|
# Partial templates carry markup only; behaviour lives in plugin JS.
|
||||||
|
local partials=()
|
||||||
|
if [[ -d "$app/plugins" ]]; then
|
||||||
|
mapfile -t partials < <(find "$app/plugins" -mindepth 3 -path '*/controllers/*' -type f -name '_*.htm' | sort)
|
||||||
|
fi
|
||||||
|
if [[ "${#partials[@]}" -gt 0 ]]; then
|
||||||
|
hits="$(grep -nHiE '<script|(^|[[:space:]"'"'"'/])style[[:space:]]*=|(^|[[:space:]"'"'"'/])on[a-z]+[[:space:]]*=' "${partials[@]}" || true)"
|
||||||
|
[[ -z "$hits" ]] || fail101 "$REASON_PARTIAL: $hits"
|
||||||
|
fi
|
||||||
|
return "$bad"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_self_test() {
|
||||||
|
bash -n "${BASH_SOURCE[0]}"
|
||||||
|
expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestPhase101Assets"}'
|
||||||
|
expect_detect fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||||||
|
{"Action":"fail","Package":"p","Test":"TestPhase101Actions"}'
|
||||||
|
expect_detect skip 2 '{"Action":"skip","Package":"p","Test":"TestPhase101AlbumsExtension"}'
|
||||||
|
expect_detect zero 3 '{"Action":"pass","Package":"git.golem15.com/golem15/summercms/modules/cabana"}'
|
||||||
|
expect_detect nonjson 4 '{"Action":"pass",'
|
||||||
|
expect_detect build 1 '{"Action":"build-fail","ImportPath":"p"}
|
||||||
|
{"Action":"pass","Package":"q","Test":"TestA"}'
|
||||||
|
expect_detect build-flag 1 '{"Action":"pass","Package":"q","Test":"TestA"}
|
||||||
|
{"Action":"fail","Package":"p","FailedBuild":"p"}'
|
||||||
|
expect_detect package 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||||||
|
{"Action":"fail","Package":"p"}'
|
||||||
|
PHASE101_REQUIRE="TestPhase101Assets TestPhase101Actions" expect_detect required 5 \
|
||||||
|
'{"Action":"pass","Package":"p","Test":"TestPhase101Assets"}'
|
||||||
|
PHASE101_REQUIRE="TestPhase101Actions" expect_detect required-failed 1 \
|
||||||
|
'{"Action":"pass","Package":"p","Test":"TestA"}
|
||||||
|
{"Action":"fail","Package":"p","Test":"TestPhase101Actions"}'
|
||||||
|
PHASE101_ALLOW="p:TestKnown" expect_detect allowed 0 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||||||
|
{"Action":"fail","Package":"p","Test":"TestKnown"}
|
||||||
|
{"Action":"fail","Package":"p"}'
|
||||||
|
PHASE101_ALLOW="p:TestKnown" expect_detect allowed-other 1 '{"Action":"fail","Package":"p","Test":"TestKnown"}
|
||||||
|
{"Action":"fail","Package":"p","Test":"TestOther"}'
|
||||||
|
PHASE101_ALLOW="p:TestKnown" expect_detect allowed-wrong-package 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||||||
|
{"Action":"fail","Package":"q","Test":"TestKnown"}'
|
||||||
|
PHASE101_ALLOW="p:TestKnown" expect_detect allowed-now-passes 6 '{"Action":"pass","Package":"p","Test":"TestKnown"}'
|
||||||
|
local flag
|
||||||
|
for flag in --self-test --go --security --postgres --spa --openapi --dist --hygiene --evidence --all; do
|
||||||
|
grep -q -- "^$flag)" "${BASH_SOURCE[0]}" || {
|
||||||
|
echo "refuse: missing mode $flag" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
# hygiene_101 passes on scratch copies of admin/src and the application
|
||||||
|
# plugins, then refuses each plant for its own reason and no other.
|
||||||
|
local scratch app
|
||||||
|
scratch="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$scratch"' RETURN
|
||||||
|
mkdir -p "$scratch/fw/admin" "$scratch/app"
|
||||||
|
cp -R "$ROOT/admin/src" "$scratch/fw/admin/src"
|
||||||
|
cp -R "$APP/plugins" "$scratch/app/plugins"
|
||||||
|
app="$scratch/app"
|
||||||
|
(hygiene_101 "$scratch/fw" "$app") >/dev/null 2>&1 || {
|
||||||
|
echo "refuse: self-test hygiene_101 rejected the clean scratch copy" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
local plant want out file others reason
|
||||||
|
for plant in parser dom-write network cookie storage script style handler; do
|
||||||
|
rm -rf "$scratch/fw/admin/src/__plant" "$app/plugins/acme"
|
||||||
|
mkdir -p "$scratch/fw/admin/src/__plant" "$app/plugins/acme/demo/assets/js" "$app/plugins/acme/demo/controllers/gadgets"
|
||||||
|
case "$plant" in
|
||||||
|
parser)
|
||||||
|
printf "export const parse = (s: string) => new DOMParser().parseFromString(s, 'text/html')\n" >"$scratch/fw/admin/src/__plant/plant.ts"
|
||||||
|
want="$REASON_PARSER"
|
||||||
|
;;
|
||||||
|
dom-write)
|
||||||
|
printf 'export const write = (s: string) => document.write(s)\n' >"$scratch/fw/admin/src/__plant/plant.ts"
|
||||||
|
want="$REASON_PARSER"
|
||||||
|
;;
|
||||||
|
network)
|
||||||
|
printf "class Plant extends HTMLElement { connectedCallback() { fetch('/backend/api/v1/x') } }\n" >"$app/plugins/acme/demo/assets/js/plant.js"
|
||||||
|
want="$REASON_ASSET"
|
||||||
|
;;
|
||||||
|
cookie)
|
||||||
|
printf 'const token = document.cookie\n' >"$app/plugins/acme/demo/assets/js/plant.js"
|
||||||
|
want="$REASON_ASSET"
|
||||||
|
;;
|
||||||
|
storage)
|
||||||
|
printf "localStorage.setItem('k', 'v')\n" >"$app/plugins/acme/demo/assets/js/plant.js"
|
||||||
|
want="$REASON_ASSET"
|
||||||
|
;;
|
||||||
|
script)
|
||||||
|
printf '<dl class="summer-stats"></dl>\n<script>alert(1)</script>\n' >"$app/plugins/acme/demo/controllers/gadgets/_plant.htm"
|
||||||
|
want="$REASON_PARTIAL"
|
||||||
|
;;
|
||||||
|
style)
|
||||||
|
printf '<p style="color:red">{{ .Data.Name }}</p>\n' >"$app/plugins/acme/demo/controllers/gadgets/_plant.htm"
|
||||||
|
want="$REASON_PARTIAL"
|
||||||
|
;;
|
||||||
|
handler)
|
||||||
|
printf '<a href="/x" onclick="steal()">x</a>\n' >"$app/plugins/acme/demo/controllers/gadgets/_plant.htm"
|
||||||
|
want="$REASON_PARTIAL"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if out="$( (hygiene_101 "$scratch/fw" "$app") 2>&1)"; then
|
||||||
|
echo "refuse: self-test hygiene_101 accepted a planted $plant" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -qF "$want" <<<"$out"; then
|
||||||
|
echo "refuse: self-test hygiene_101 rejected the $plant plant without naming its rule: $out" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for reason in "$REASON_PARSER" "$REASON_ASSET" "$REASON_PARTIAL"; do
|
||||||
|
if [[ "$reason" != "$want" ]] && grep -qF "$reason" <<<"$out"; then
|
||||||
|
echo "refuse: self-test hygiene_101 rejected the $plant plant for another rule: $out" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
# Markup that only resembles a handler or style attribute is not refused.
|
||||||
|
rm -rf "$scratch/fw/admin/src/__plant" "$app/plugins/acme"
|
||||||
|
mkdir -p "$app/plugins/acme/demo/controllers/gadgets"
|
||||||
|
printf '<p class="summer-stat" data-action="x" aria-label="on=1">{{ trans "acme.demo::lang.button" }}</p>\n' >"$app/plugins/acme/demo/controllers/gadgets/_clean.htm"
|
||||||
|
(hygiene_101 "$scratch/fw" "$app") >/dev/null 2>&1 || {
|
||||||
|
echo "refuse: self-test hygiene_101 rejected clean partial markup" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "phase10.1 self-test passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_go() {
|
||||||
|
(cd "$ROOT" && go vet ./...)
|
||||||
|
phase101_go "$ROOT" ./...
|
||||||
|
(cd "$APP" && go vet ./... "${APP_PLUGINS[@]}")
|
||||||
|
PHASE101_ALLOW="$KNOWN_APP_FAILURES" phase101_go "$APP" ./... "${APP_PLUGINS[@]}"
|
||||||
|
echo "phase10.1 go passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_security() {
|
||||||
|
phase101_tests "$ROOT" ./modules/cabana TestPhase10CSRF TestPhase09PermissionMatrix TestPhase101Assets \
|
||||||
|
TestPhase101PartialSanitizer TestPhase101Actions TestPhase101FormExtensionSchema TestPhase101PartialSchema TestPhase101Toolbar
|
||||||
|
PHASE101_REQUIRE="TestPhase101BoardwalkExports" phase101_go "$ROOT" ./modules/boardwalk
|
||||||
|
phase101_tests "$APP" ./plugins/golem15/fonoteka TestPhase09SecurityRoutes
|
||||||
|
echo "phase10.1 security passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_postgres() {
|
||||||
|
phase101_tests "$ROOT" ./modules/cabana TestPhase10OpenAPIConformance TestPhase101Actions
|
||||||
|
phase101_tests "$APP" ./plugins/golem15/fonoteka TestPhase101AlbumsExtension TestPhase101AlbumsSmoke TestPhase10Controllers \
|
||||||
|
TestPhase10ControllerCopy TestAlbumsAdminForm TestAlbumsAdminList TestPhase10AssembledAcceptance
|
||||||
|
echo "phase10.1 postgres passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_spa() {
|
||||||
|
npm --prefix "$ROOT/admin" ci --no-audit --no-fund
|
||||||
|
npm --prefix "$ROOT/admin" run typecheck
|
||||||
|
local log
|
||||||
|
log="$(mktemp)"
|
||||||
|
set +e
|
||||||
|
npm --prefix "$ROOT/admin" test >"$log" 2>&1
|
||||||
|
local rc=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$rc" -ne 0 ]] || grep -qE 'No test files found|Unhandled (Errors|Rejection)|FAIL ' "$log"; then
|
||||||
|
tail -n 60 "$log" >&2
|
||||||
|
rm -f "$log"
|
||||||
|
echo "refuse: admin Vitest run failed (exit $rc)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -E 'Test Files|Tests ' "$log" || true
|
||||||
|
rm -f "$log"
|
||||||
|
echo "phase10.1 spa passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_openapi() {
|
||||||
|
"$ROOT/scripts/check-admin-openapi.sh" --check
|
||||||
|
phase101_tests "$ROOT" ./modules/cabana TestPhase10OpenAPIConformance TestPhase09ContractInventory
|
||||||
|
echo "phase10.1 openapi passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_dist() {
|
||||||
|
"$ROOT/scripts/check-admin-dist.sh"
|
||||||
|
echo "phase10.1 dist passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_hygiene() {
|
||||||
|
"$ROOT/scripts/check-phase10.sh" --hygiene
|
||||||
|
hygiene_101 "$ROOT" "$APP"
|
||||||
|
echo "phase10.1 hygiene passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_evidence() {
|
||||||
|
[[ -f "$REVIEW" && -f "$VALIDATION" ]] || {
|
||||||
|
echo "refuse: security review or validation file is missing" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
python3 - "$REVIEW" "$VALIDATION" <<'PY'
|
||||||
|
import pathlib, re, sys
|
||||||
|
review = pathlib.Path(sys.argv[1]).read_text()
|
||||||
|
validation = pathlib.Path(sys.argv[2]).read_text()
|
||||||
|
required = [f"T-10.1-{i:02d}" for i in range(1, 23)] + ["T-10.1-SC"]
|
||||||
|
lines = review.splitlines()
|
||||||
|
removal = [line for line in lines if line.startswith("| RC-")]
|
||||||
|
for item in required:
|
||||||
|
rows = [line for line in lines if line.startswith("| " + item + " ")]
|
||||||
|
if len(rows) != 1:
|
||||||
|
print(f"refuse: review has {len(rows)} threat rows for {item}, want 1", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
row = rows[0]
|
||||||
|
cells = [cell.strip().lower() for cell in row.strip("|").split("|")]
|
||||||
|
high = "high" in cells
|
||||||
|
mitigated = "mitigate" in cells
|
||||||
|
if high and mitigated:
|
||||||
|
if not re.search(r"Test[A-Z][A-Za-z0-9]+|check-phase[\d.]+\.sh|check-admin-|tests/", row):
|
||||||
|
print(f"refuse: high threat {item} names no failing-when-broken test or gate stage", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not any(re.search(re.escape(item) + r"(?![0-9A-Za-z])", line) for line in removal):
|
||||||
|
print(f"refuse: high threat {item} has no removal check row", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not re.search(r"^nyquist_compliant: true$", validation, re.M):
|
||||||
|
print("refuse: validation is not nyquist_compliant", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
for line in validation.splitlines():
|
||||||
|
if line.startswith("|") and "pending" in line.lower():
|
||||||
|
print("refuse: validation row still pending: " + line, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if "ADMIN-07" not in validation:
|
||||||
|
print("refuse: validation does not name ADMIN-07", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print("phase10.1 evidence files passed")
|
||||||
|
PY
|
||||||
|
run_security
|
||||||
|
run_postgres
|
||||||
|
run_openapi
|
||||||
|
echo "phase10.1 evidence passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
--self-test) run_self_test ;;
|
||||||
|
--go) run_go ;;
|
||||||
|
--security) run_security ;;
|
||||||
|
--postgres) run_postgres ;;
|
||||||
|
--spa) run_spa ;;
|
||||||
|
--openapi) run_openapi ;;
|
||||||
|
--dist) run_dist ;;
|
||||||
|
--hygiene) run_hygiene ;;
|
||||||
|
--evidence) run_evidence ;;
|
||||||
|
--all)
|
||||||
|
run_self_test
|
||||||
|
run_go
|
||||||
|
run_security
|
||||||
|
run_postgres
|
||||||
|
run_spa
|
||||||
|
run_openapi
|
||||||
|
run_dist
|
||||||
|
run_hygiene
|
||||||
|
run_evidence
|
||||||
|
echo "phase10.1 all passed"
|
||||||
|
;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user