forgedock 1.0.21 → 1.1.1
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 +227 -174
- package/bin/cinema.mjs +320 -0
- package/bin/demo.mjs +386 -0
- package/bin/engine/phases.mjs +148 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +27 -0
- package/bin/engine-cli.mjs +50 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +693 -1393
- package/bin/init-detect.mjs +95 -0
- package/bin/journey.mjs +783 -0
- package/bin/runner.mjs +847 -0
- package/bin/settings-hook.mjs +112 -0
- package/bin/tui.mjs +6 -3
- package/commands/autopilot.md +4 -4
- package/commands/cleanup.md +85 -39
- package/commands/deploy-info.md +11 -5
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +2 -1
- package/commands/issue.md +1 -1
- package/commands/optimize.md +1 -1
- package/commands/orchestrate.md +346 -34
- package/commands/quality-gate.md +168 -80
- package/commands/review-pr-agents.md +69 -45
- package/commands/review-pr.md +54 -6
- package/commands/scope.md +11 -3
- package/commands/security-audit.md +24 -16
- package/commands/sync-ecosystem.md +2 -2
- package/commands/work-on/build/architect.md +47 -3
- package/commands/work-on/build/context.md +36 -5
- package/commands/work-on/build/implement.md +16 -4
- package/commands/work-on/build/validate.md +54 -8
- package/commands/work-on/build.md +27 -9
- package/commands/work-on/close.md +131 -3
- package/commands/work-on/investigate.md +24 -1
- package/commands/work-on/review.md +24 -0
- package/commands/work-on-monolithic.md +1 -0
- package/commands/work-on.md +271 -11
- package/docs/BRAND.md +6 -2
- package/docs/CONFIG.md +39 -34
- package/docs/site/.vitepress/config.mjs +85 -0
- package/docs/site/annotations-explained.md +55 -0
- package/docs/site/command-learning-path.md +104 -0
- package/docs/site/command-reference.md +3 -0
- package/docs/site/getting-started.md +42 -13
- package/docs/site/index.md +1 -1
- package/docs/site/troubleshooting.md +208 -0
- package/docs/spec/forge-protocol-v1.md +768 -0
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/examples/forgedock-demo/README.md +113 -0
- package/examples/forgedock-demo/bootstrap.sh +125 -0
- package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
- package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
- package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
- package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
- package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
- package/examples/forgedock-demo/labels.json +21 -0
- package/examples/forgedock-demo/package.json +16 -0
- package/examples/forgedock-demo/scripts/smoke.js +61 -0
- package/examples/forgedock-demo/src/auth.js +28 -0
- package/examples/forgedock-demo/src/db.js +66 -0
- package/examples/forgedock-demo/src/routes/notes.js +64 -0
- package/examples/forgedock-demo/src/server.js +79 -0
- package/package.json +13 -3
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
- package/templates/forge.yaml.minimal +44 -0
|
@@ -31,6 +31,11 @@ gh pr diff [PR_NUMBER] --name-only
|
|
|
31
31
|
gh pr diff [PR_NUMBER]
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
**Hot-spot prior**:
|
|
35
|
+
[CHURN_CONTEXT]
|
|
36
|
+
|
|
37
|
+
If a file you are reviewing is listed above as a hot-spot, apply deeper scrutiny to it — high historical churn correlates with defect density. Prefer tracing that file's full code paths (Evidence-Based Review Protocol §2) over a quick pattern scan, and weight ambiguous findings in hot-spot files toward LIKELY rather than POSSIBLE.
|
|
38
|
+
|
|
34
39
|
### 2. Dynamic Exploration
|
|
35
40
|
- From each changed file, follow imports and function calls
|
|
36
41
|
- Trace data flows across service boundaries (API → Redis → Worker)
|
|
@@ -201,9 +206,10 @@ For each changed file, check:
|
|
|
201
206
|
|
|
202
207
|
FastAPI's default validation error response exposes internal field names, types, and constraints. Attackers can enumerate the complete request schema by sending malformed payloads.
|
|
203
208
|
```bash
|
|
204
|
-
|
|
209
|
+
while IFS= read -r f; do
|
|
210
|
+
[ -z "$f" ] && continue
|
|
205
211
|
grep -rn "RequestValidationError\|validation_exception_handler\|@app.exception_handler" services/api/app/main.py services/api/app/ 2>/dev/null | head -5
|
|
206
|
-
done
|
|
212
|
+
done <<< "$MODEL_FILES"
|
|
207
213
|
# If no custom handler found AND the PR adds new Pydantic models or FastAPI endpoints:
|
|
208
214
|
# LIKELY MEDIUM — FastAPI default exposes internal field names/types/constraints to any caller
|
|
209
215
|
# Safe pattern: catch RequestValidationError and return only {"detail": "Invalid request"} or a sanitized message
|
|
@@ -216,12 +222,13 @@ For each changed file, check:
|
|
|
216
222
|
|
|
217
223
|
Unauthenticated health endpoints are expected to be public — the Auth agent's auth-dependency check does not apply. The relevant question is whether they reveal more than `{"status": "ok"}`.
|
|
218
224
|
```bash
|
|
219
|
-
|
|
225
|
+
while IFS= read -r f; do
|
|
226
|
+
[ -z "$f" ] && continue
|
|
220
227
|
# Find unauthenticated endpoints (no Depends() in signature) and check return content
|
|
221
228
|
grep -A25 "@router\.\(get\|head\)\|async def health\|async def ping\|async def live\|async def ready\|async def status" "$f" | \
|
|
222
229
|
grep -iE "version|host|port|redis|postgres|db_url|tesseract|pillow|PIL\.__version__|software|engine|server" && \
|
|
223
230
|
echo "SEC: health endpoint in $f returns operational details. Reveals software versions (enables CVE targeting) or connectivity state (enables DoS confirmation). Safe pattern: return only {\"status\": \"ok\"} or {\"status\": \"degraded\"} — no version strings, no host/port, no connectivity details."
|
|
224
|
-
done
|
|
231
|
+
done <<< "$HEALTH_FILES"
|
|
225
232
|
```
|
|
226
233
|
**Confidence**: `LIKELY` — version strings may be intentionally scoped to internal-only endpoints; verify auth status of the endpoint.
|
|
227
234
|
**Severity**: MEDIUM for software version strings (direct CVE targeting input); LOW for DB/Redis connectivity state (useful for DoS confirmation, not exploit).
|
|
@@ -231,10 +238,11 @@ For each changed file, check:
|
|
|
231
238
|
|
|
232
239
|
Git SHAs and deployment topology exposed via response headers enable targeted CVE research and attack planning.
|
|
233
240
|
```bash
|
|
234
|
-
|
|
241
|
+
while IFS= read -r f; do
|
|
242
|
+
[ -z "$f" ] && continue
|
|
235
243
|
grep -nE "X-App-Version|X-Deployment-Color|X-.*-SHA|X-.*-Build|X-.*-Commit|Server:|X-Powered-By" "$f" 2>/dev/null && \
|
|
236
244
|
echo "SEC: response header in $f leaks infrastructure details. Git SHA enables targeted CVE diffing against public commits. Deployment color reveals blue/green topology. Safe pattern: omit these headers or gate them behind an internal-only middleware."
|
|
237
|
-
done
|
|
245
|
+
done <<< "$MIDDLEWARE_FILES"
|
|
238
246
|
```
|
|
239
247
|
**Confidence**: `LIKELY` — header may be intentional for internal observability tooling behind an auth gate; verify it applies to all responses.
|
|
240
248
|
**Severity**: LOW — information disclosure accelerates other attacks but is not directly exploitable without an additional vulnerability.
|
|
@@ -259,11 +267,12 @@ CONFIG_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "(traefik/|infra/ngi
|
|
|
259
267
|
|
|
260
268
|
**A. Port bind address audit** (when `docker-compose*.yml` is in diff — trigger: `$INFRA_FILES` non-empty)
|
|
261
269
|
```bash
|
|
262
|
-
|
|
270
|
+
while IFS= read -r f; do
|
|
271
|
+
[ -z "$f" ] && continue
|
|
263
272
|
# Flag non-web ports bound to 0.0.0.0 (no explicit host binding = binds to 0.0.0.0 by default)
|
|
264
273
|
grep -nE "^\s+- \"[0-9]+:[0-9]+\"" "$f" | grep -v "127\.0\.0\.1\|::1" | grep -vE "\"(80|443):" && \
|
|
265
274
|
echo "SEC: port published on 0.0.0.0 in $f — verify UFW/firewall is the ONLY protection. Docker daemon restart can reset iptables and expose the port directly."
|
|
266
|
-
done
|
|
275
|
+
done <<< "$INFRA_FILES"
|
|
267
276
|
```
|
|
268
277
|
**Confidence**: `LIKELY` — a firewall rule may exist that provides protection; flag for verification rather than hard-block.
|
|
269
278
|
**Severity**: HIGH for database/internal service ports (5432, 6432, 6379); MEDIUM for other non-web ports.
|
|
@@ -271,10 +280,11 @@ done
|
|
|
271
280
|
|
|
272
281
|
**B. Missing USER directive** (when any `Dockerfile` is in diff — trigger: `$DOCKERFILE_FILES` non-empty)
|
|
273
282
|
```bash
|
|
274
|
-
|
|
283
|
+
while IFS= read -r f; do
|
|
284
|
+
[ -z "$f" ] && continue
|
|
275
285
|
grep -q "^USER " "$f" || \
|
|
276
286
|
echo "SEC: CONFIRMED — no USER directive in $f. Container runs as UID 0. For services that process user-supplied URLs or untrusted input (worker, Playwright), running as root significantly lowers the bar for container escape."
|
|
277
|
-
done
|
|
287
|
+
done <<< "$DOCKERFILE_FILES"
|
|
278
288
|
```
|
|
279
289
|
**Confidence**: `CONFIRMED` — absence of USER directive is an objective, verifiable fact.
|
|
280
290
|
**Severity**: HIGH for services processing untrusted input; MEDIUM for internal-only services.
|
|
@@ -282,10 +292,11 @@ done
|
|
|
282
292
|
|
|
283
293
|
**C. Response header information disclosure** (when Python middleware/main.py adds response headers — trigger: `$HEADER_FILES` non-empty)
|
|
284
294
|
```bash
|
|
285
|
-
|
|
295
|
+
while IFS= read -r f; do
|
|
296
|
+
[ -z "$f" ] && continue
|
|
286
297
|
grep -nE "X-App-Version|X-Deployment-Color|Server:|X-Powered-By|X-.*-SHA|X-.*-Build" "$f" 2>/dev/null && \
|
|
287
298
|
echo "SEC: response header in $f leaks infrastructure details. Git SHA enables targeted CVE diffing against public commits. Deployment color reveals blue/green topology to attackers."
|
|
288
|
-
done
|
|
299
|
+
done <<< "$HEADER_FILES"
|
|
289
300
|
```
|
|
290
301
|
**Confidence**: `CONFIRMED` — header name reveals infra details by definition.
|
|
291
302
|
**Severity**: MEDIUM — information disclosure accelerates other attacks but is not directly exploitable.
|
|
@@ -293,11 +304,12 @@ done
|
|
|
293
304
|
|
|
294
305
|
**D. CI security scan gating** (when `.github/workflows/*.yml` is in diff — trigger: `$WORKFLOW_FILES` non-empty)
|
|
295
306
|
```bash
|
|
296
|
-
|
|
307
|
+
while IFS= read -r f; do
|
|
308
|
+
[ -z "$f" ] && continue
|
|
297
309
|
# Find security scanner steps with continue-on-error: true
|
|
298
310
|
grep -B5 "continue-on-error: true" "$f" | grep -iE "trivy|snyk|security|cve|scan|grype|dockle" && \
|
|
299
311
|
echo "SEC: security scanner in $f has continue-on-error: true — CVEs will be logged but will NEVER block a deploy. Remove continue-on-error or add a severity threshold."
|
|
300
|
-
done
|
|
312
|
+
done <<< "$WORKFLOW_FILES"
|
|
301
313
|
```
|
|
302
314
|
**Confidence**: `CONFIRMED` — `continue-on-error: true` on a security scanner step is objectively a non-blocking scan.
|
|
303
315
|
**Severity**: HIGH — security scanner provides zero protection when it cannot block.
|
|
@@ -305,10 +317,11 @@ done
|
|
|
305
317
|
|
|
306
318
|
**E. In-memory rate limiter** (when TypeScript files are in diff — trigger: `$TS_FILES` non-empty)
|
|
307
319
|
```bash
|
|
308
|
-
|
|
320
|
+
while IFS= read -r f; do
|
|
321
|
+
[ -z "$f" ] && continue
|
|
309
322
|
grep -nE "new Map\(\)|=\s*\{\}|= new Map" "$f" 2>/dev/null | grep -iE "rate|limit|attempt|count|throttle|window" && \
|
|
310
323
|
echo "SEC: in-memory rate limiter detected in $f — resets on every container restart, not shared across blue/green replicas. Use Redis (via Upstash, ioredis, or the existing Redis client) for distributed rate limiting."
|
|
311
|
-
done
|
|
324
|
+
done <<< "$TS_FILES"
|
|
312
325
|
```
|
|
313
326
|
**Confidence**: `LIKELY` — in-memory store may be intentional for non-critical paths; flag for verification.
|
|
314
327
|
**Severity**: MEDIUM — bypass requires restarting the container or hitting a different replica; not trivially exploitable but provides no protection in blue/green deploys.
|
|
@@ -316,10 +329,11 @@ done
|
|
|
316
329
|
|
|
317
330
|
**F. Cookie security flags** (when TypeScript files are in diff — trigger: `$TS_FILES` non-empty)
|
|
318
331
|
```bash
|
|
319
|
-
|
|
332
|
+
while IFS= read -r f; do
|
|
333
|
+
[ -z "$f" ] && continue
|
|
320
334
|
grep -nE "httpOnly:\s*false|secure:\s*false|sameSite.*['\"]none['\"]" "$f" 2>/dev/null && \
|
|
321
335
|
echo "SEC: cookie in $f has weakened security flag. httpOnly: false exposes cookie to JS (XSS risk). secure: false allows transmission over HTTP. sameSite: none without Secure enables CSRF."
|
|
322
|
-
done
|
|
336
|
+
done <<< "$TS_FILES"
|
|
323
337
|
```
|
|
324
338
|
**Confidence**: `CONFIRMED` — explicit `false`/`none` values are objective findings.
|
|
325
339
|
**Severity**: HIGH for `httpOnly: false` (direct XSS vector for session cookies); MEDIUM for `secure: false` or `sameSite: none`.
|
|
@@ -327,33 +341,36 @@ done
|
|
|
327
341
|
|
|
328
342
|
**G. Fallback credential scan in config files** (when config files are in diff — trigger: `$CONFIG_FILES` non-empty)
|
|
329
343
|
```bash
|
|
330
|
-
|
|
344
|
+
while IFS= read -r f; do
|
|
345
|
+
[ -z "$f" ] && continue
|
|
331
346
|
# Shell-style fallback: ${VAR:-default} — flag non-trivial fallback values that may be credentials
|
|
332
347
|
grep -nE "\$\{[A-Z_]+:-[^}]{4,}\}" "$f" | grep -ivE "(localhost|127\.0\.0\.1|0\.0\.0\.0|true|false|^[0-9]+$|/tmp|/var|/etc)" && \
|
|
333
348
|
echo "SEC: potential credential fallback in $f — verify fallback value is safe if env var is absent"
|
|
334
|
-
done
|
|
349
|
+
done <<< "$CONFIG_FILES"
|
|
335
350
|
```
|
|
336
351
|
For each hit: read the fallback value.
|
|
337
352
|
- Contains `$apr1$`, `$2b$`, `$bcrypt$`, `$argon2`, or hash-like long alphanumeric with `$` separators: **CONFIRMED HIGH** — htpasswd/bcrypt placeholder; active if env var is absent.
|
|
338
353
|
- Is `admin`, `password`, `changeme`, `secret`, `test`, or similar weak string: **CONFIRMED HIGH** — default credential.
|
|
339
354
|
- Is empty string (`${VAR:-}` or `${VAR:-""}`): **CONFIRMED HIGH** if field controls authentication.
|
|
340
355
|
|
|
341
|
-
**Required cross-checks when a credential fallback is found**: (1) Is the env var required in `app/env_validation.py`? (2) Is it in `scripts/decrypt-secrets.sh` ENV_MAPPING? (3) Is it marked required in `.env.example`? If ANY missing: the insecure fallback may be active in some environments.
|
|
356
|
+
**Required cross-checks when a credential fallback is found**: (1) Is the env var required in `app/env_validation.py`? (2) Is it in `scripts/decrypt-secrets.sh` ENV_MAPPING (if configured in your SOPS deploy chain — skip if absent)? (3) Is it marked required in `.env.example`? If ANY missing: the insecure fallback may be active in some environments.
|
|
342
357
|
**Confidence**: `CONFIRMED` — fallback value is objectively verifiable.
|
|
343
358
|
**Severity**: HIGH — insecure fallback credential is equivalent to a hardcoded credential for any environment missing the env var.
|
|
344
359
|
**Evidence**: Config files commonly use `${VAR:-placeholder}` patterns during development, where the placeholder is a sample credential or hash. If the env var is absent in any environment (staging, non-production, first deploy before secrets are injected), the placeholder becomes the active credential. Also covered by INFRA agent item 13 (deployment context + ENV_MAPPING cross-check); this item extends coverage to PRs not classified as INFRA domain.
|
|
345
360
|
|
|
346
361
|
**H. Runtime UID × volume ownership** (when any Dockerfile is in diff AND diff contains `USER`, `su-exec`, `gosu`, or `setuid` — trigger: `$DOCKERFILE_FILES` non-empty AND UID change detected)
|
|
347
362
|
```bash
|
|
348
|
-
UID_CHANGE=$(
|
|
363
|
+
UID_CHANGE=$(while IFS= read -r f; do
|
|
364
|
+
[ -z "$f" ] && continue
|
|
349
365
|
gh pr diff [PR_NUMBER] -- "$f" 2>/dev/null | grep -E '^\+.*(USER\s+[^0]|su-exec|gosu|setuid)' | grep -v '^+++' && break
|
|
350
|
-
done)
|
|
366
|
+
done <<< "$DOCKERFILE_FILES")
|
|
351
367
|
if [ -n "$UID_CHANGE" ]; then
|
|
352
368
|
echo "UID change detected — cross-referencing named volume mounts..."
|
|
353
369
|
COMPOSE_FILES=$(find . -maxdepth 2 -name "docker-compose*.yml" 2>/dev/null | sort)
|
|
354
|
-
|
|
370
|
+
while IFS= read -r cf; do
|
|
371
|
+
[ -z "$cf" ] && continue
|
|
355
372
|
grep -A2 "volumes:" "$cf" | grep -E "^\s+\w.*:" | grep -v "^--$" && echo " ^ in $cf"
|
|
356
|
-
done
|
|
373
|
+
done <<< "$COMPOSE_FILES"
|
|
357
374
|
fi
|
|
358
375
|
```
|
|
359
376
|
For each named volume found: determine its container mount point. Named volumes are created as **root-owned** by Docker at first use — a privilege drop (root → UID N) without a `chown` before the drop will silently break any service path that calls `mkdir`, `write_bytes`, `open(... 'w')`, or `os.makedirs` under that mount point.
|
|
@@ -784,14 +801,14 @@ CRITICAL: This is a billing system. Double-spend = revenue loss.
|
|
|
784
801
|
```
|
|
785
802
|
If `WHERE reserved_by IS NULL` appears in a SELECT that is followed by a separate UPDATE (not in the same atomic statement), this is a CONFIRMED HIGH finding — two concurrent sessions can both pass the read check before either writes.
|
|
786
803
|
|
|
787
|
-
8. **Cross-service flag staleness**: When a discount or pricing flag (
|
|
804
|
+
8. **Cross-service flag staleness**: When a discount or pricing flag (e.g. `has_active_subscription`, a `discount_type` field) is set by the API layer at job submission and read by the Worker layer at billing time, verify the flag is re-validated at debit time — not trusted from the queued job payload.
|
|
788
805
|
|
|
789
806
|
Search for discount flags passed through Redis/job payloads:
|
|
790
807
|
```bash
|
|
791
|
-
grep -rn "
|
|
808
|
+
grep -rn "discount.*flag\|flag.*discount\|subscription.*flag\|plan_type\|discount_type" services/api/ services/worker/ | head -20
|
|
792
809
|
# If the flag flows through a job payload and is not re-validated at billing: CONFIRMED HIGH
|
|
793
810
|
```
|
|
794
|
-
A race window exists when a flag is checked at submission but consumed at billing: the underlying condition (e.g.,
|
|
811
|
+
A race window exists when a flag is checked at submission but consumed at billing: the underlying condition (e.g., subscription status, entitlement) may have changed between the two operations. The fix must re-validate the flag atomically at the point of debit, not rely on a stale value from the job payload.
|
|
795
812
|
|
|
796
813
|
## Safe Patterns in This Codebase
|
|
797
814
|
```bash
|
|
@@ -1300,8 +1317,8 @@ You are auditing PR #[PR_NUMBER] for database changes in [PROJECT_NAME].
|
|
|
1300
1317
|
a. List ALL `*.sql` files in `infra/migrations/` on the PR's target branch (use `git ls-tree` or `ls`)
|
|
1301
1318
|
b. Extract the 4-digit numeric prefix from each filename (the leading digits before the first `_`)
|
|
1302
1319
|
c. Identify any prefix that appears more than once
|
|
1303
|
-
d.
|
|
1304
|
-
e. **DEPLOY GATE — CRITICAL**: If ANY non-
|
|
1320
|
+
d. Identify if the project maintains a grandfathered-duplicates allowlist (e.g., a config file or comment block listing known-safe legacy duplicate prefixes). If one exists, cross-reference against it. <!-- Updated: forge#1349 — removed stale validate-migration-order.sh reference (script does not exist in ForgeDock) -->
|
|
1321
|
+
e. **DEPLOY GATE — CRITICAL**: If ANY non-allowlisted duplicate prefix exists → flag as **CRITICAL BLOCKER** and reject the PR. **Do NOT apply migration runner reasoning here.** Deploy gates that enforce migration ordering hard-fail on any non-allowed duplicate regardless of whether the migration runner executes files correctly. The runner may handle duplicate filenames fine; the deploy script does not. A PR that passes this reasoning trap ("unique filenames, so the runner is safe") will still halt production deploy. The only safe classification is CRITICAL BLOCKER.
|
|
1305
1322
|
f. Additionally: if the PR adds new migration files, verify their prefixes don't collide with existing files in the directory
|
|
1306
1323
|
9. **FK and CHECK constraints**: New tables should have appropriate foreign keys and CHECK constraints. Missing FK allows orphaned rows; missing CHECK allows invalid enum values.
|
|
1307
1324
|
10. **asyncpg gotchas**:
|
|
@@ -1414,17 +1431,19 @@ If no infra context is configured above, derive the deployment model from the ch
|
|
|
1414
1431
|
OLD_VALUES=$(gh pr diff [PR_NUMBER] | grep "^\-" | grep -oP '"status"\s*:\s*"\K[^"]+' | sort -u)
|
|
1415
1432
|
|
|
1416
1433
|
# Search all non-application consumer locations for those old values
|
|
1417
|
-
|
|
1434
|
+
while IFS= read -r val; do
|
|
1435
|
+
[ -z "$val" ] && continue
|
|
1418
1436
|
echo "=== Searching for old response value: $val ==="
|
|
1419
1437
|
grep -rn "$val" scripts/ infra/ .github/ traefik/ 2>/dev/null | grep -v "^Binary"
|
|
1420
1438
|
grep -rn "$val" docker-compose*.yml 2>/dev/null
|
|
1421
|
-
done
|
|
1439
|
+
done <<< "$OLD_VALUES"
|
|
1422
1440
|
|
|
1423
1441
|
# Also search for endpoint path references in consumer locations
|
|
1424
1442
|
HEALTH_PATHS=$(gh pr diff [PR_NUMBER] | grep "^\+" | grep -oP '@\w+\.(?:get|post)\("\K[^"]+(?:health|status|ping|ready|live|readiness|liveness)[^"]*')
|
|
1425
|
-
|
|
1443
|
+
while IFS= read -r path; do
|
|
1444
|
+
[ -z "$path" ] && continue
|
|
1426
1445
|
grep -rn "$path" scripts/ infra/ .github/ traefik/ docker-compose*.yml 2>/dev/null | grep -v "^Binary"
|
|
1427
|
-
done
|
|
1446
|
+
done <<< "$HEALTH_PATHS"
|
|
1428
1447
|
```
|
|
1429
1448
|
|
|
1430
1449
|
**For each hit, classify:**
|
|
@@ -1522,13 +1541,14 @@ If no infra context is configured above, derive the deployment model from the ch
|
|
|
1522
1541
|
|
|
1523
1542
|
**Scan ALL `{{` occurrences in every `script:` block of every `appleboy/ssh-action` step in changed files:**
|
|
1524
1543
|
```bash
|
|
1525
|
-
|
|
1544
|
+
while IFS= read -r f; do
|
|
1545
|
+
[ -z "$f" ] && continue
|
|
1526
1546
|
if grep -q "appleboy/ssh-action" "$f"; then
|
|
1527
1547
|
echo "=== appleboy/ssh-action found in $f ==="
|
|
1528
1548
|
# Show all {{ occurrences with line numbers
|
|
1529
1549
|
grep -n "{{" "$f"
|
|
1530
1550
|
fi
|
|
1531
|
-
done
|
|
1551
|
+
done < <(gh pr diff [PR_NUMBER] --name-only | grep -E '\.github/workflows/.*\.yml$')
|
|
1532
1552
|
```
|
|
1533
1553
|
|
|
1534
1554
|
**For every `{{` found, classify and flag:**
|
|
@@ -1549,10 +1569,11 @@ If no infra context is configured above, derive the deployment model from the ch
|
|
|
1549
1569
|
**Scan changed config files for credential fallbacks:**
|
|
1550
1570
|
```bash
|
|
1551
1571
|
CONFIG_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "(traefik/|infra/nginx/|infra/|\.github/workflows/).*(\.ya?ml|\.toml|\.json|\.conf|\.ini)$" | head -20)
|
|
1552
|
-
|
|
1572
|
+
while IFS= read -r f; do
|
|
1573
|
+
[ -z "$f" ] && continue
|
|
1553
1574
|
# Shell-style fallback: ${VAR:-default} — filter out safe non-credential defaults
|
|
1554
1575
|
grep -nE "\$\{[A-Z_]+:-[^}]{4,}\}" "$f" | grep -ivE "(localhost|127\.0\.0\.1|0\.0\.0\.0|true|false|^[0-9]+$|/tmp|/var|/etc)" | head -20
|
|
1555
|
-
done
|
|
1576
|
+
done <<< "$CONFIG_FILES"
|
|
1556
1577
|
```
|
|
1557
1578
|
|
|
1558
1579
|
**For each hit, classify the fallback value:**
|
|
@@ -1567,7 +1588,7 @@ If no infra context is configured above, derive the deployment model from the ch
|
|
|
1567
1588
|
|
|
1568
1589
|
**Cross-check when a credential fallback is found:**
|
|
1569
1590
|
1. Is the corresponding env var required (not optional) in `app/env_validation.py` (or equivalent startup validation)?
|
|
1570
|
-
2. Is the env var in `scripts/decrypt-secrets.sh` ENV_MAPPING (delivered from SOPS to `.env.production`)?
|
|
1591
|
+
2. Is the env var in `scripts/decrypt-secrets.sh` ENV_MAPPING (if configured in your SOPS deploy chain — skip if absent; delivered from SOPS to `.env.production`)?
|
|
1571
1592
|
3. Is the env var documented with a "REQUIRED" note in `.env.example`?
|
|
1572
1593
|
|
|
1573
1594
|
If ANY of these three are missing: the env var may legitimately be absent in some environments, making the insecure fallback active. Flag as **CONFIRMED HIGH** with all three cross-check results.
|
|
@@ -1588,9 +1609,10 @@ If no infra context is configured above, derive the deployment model from the ch
|
|
|
1588
1609
|
**Scan for host-side database tool invocations:**
|
|
1589
1610
|
```bash
|
|
1590
1611
|
DEPLOY_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "(\.github/workflows/.*\.yml$|scripts/deploy.*\.sh$|docker-entrypoint.*\.sh$)")
|
|
1591
|
-
|
|
1612
|
+
while IFS= read -r f; do
|
|
1613
|
+
[ -z "$f" ] && continue
|
|
1592
1614
|
grep -n "\bpsql\b\|\bcreatedb\b\|\bpg_dump\b\|\bpg_restore\b" "$f" | grep -v "docker exec" | head -20
|
|
1593
|
-
done
|
|
1615
|
+
done <<< "$DEPLOY_FILES"
|
|
1594
1616
|
```
|
|
1595
1617
|
|
|
1596
1618
|
**For each match, classify:**
|
|
@@ -1633,15 +1655,17 @@ If no infra context is configured above, derive the deployment model from the ch
|
|
|
1633
1655
|
COMPOSE_INFRA_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "docker-compose.*\.yml$")
|
|
1634
1656
|
|
|
1635
1657
|
# For each config file: read the full file (not just the diff) to understand the structure
|
|
1636
|
-
|
|
1658
|
+
while IFS= read -r f; do
|
|
1659
|
+
[ -z "$f" ] && continue
|
|
1637
1660
|
echo "=== Reading full config structure: $f ==="
|
|
1638
1661
|
cat "$f"
|
|
1639
|
-
done
|
|
1662
|
+
done <<< "$CONFIG_TOOL_FILES"
|
|
1640
1663
|
|
|
1641
1664
|
# Identify tool version from docker-compose (for version-aware schema reasoning)
|
|
1642
|
-
|
|
1665
|
+
while IFS= read -r f; do
|
|
1666
|
+
[ -z "$f" ] && continue
|
|
1643
1667
|
grep -E "image:\s*(traefik|nginx|haproxy|envoy|caddy|terraform):" "$f" 2>/dev/null && echo " ^ in $f"
|
|
1644
|
-
done
|
|
1668
|
+
done <<< "$COMPOSE_INFRA_FILES"
|
|
1645
1669
|
```
|
|
1646
1670
|
|
|
1647
1671
|
**Severity classification**:
|
package/commands/review-pr.md
CHANGED
|
@@ -147,7 +147,7 @@ echo "=== DOMAINS ==="
|
|
|
147
147
|
echo "$DIFF" | grep -cE "SessionUser|CurrentUser|get_current_user|jwt|oauth|login|logout|Depends\(get_|x.forwarded.for|x_forwarded_for|forwarded_for|rate.limit.*ip|ip.*rate.limit|algorithm.*HS256|algorithm.*RS256|NEXTAUTH_SECRET|JWT_SECRET|admin.proxy" && echo "AUTH" || true
|
|
148
148
|
echo "$DIFF" | grep -cE "credit|balance|debit|reconcil|tier_cost|pricing|charge|refund|stripe|subscription" && echo "BILLING" || true
|
|
149
149
|
echo "$DIFF" | grep -cE "scrape|tier.*escalat|proxy|anti_bot|stealth|playwright|playbook" && echo "SCRAPING" || true
|
|
150
|
-
echo "$DIFF" | grep -cE "FOR UPDATE|atomic|transaction|pipeline|MULTI|distributed_lock|acquire_lock|reserved_by|promo.*claim|voucher.*redeem
|
|
150
|
+
echo "$DIFF" | grep -cE "FOR UPDATE|atomic|transaction|pipeline|MULTI|distributed_lock|acquire_lock|reserved_by|promo.*claim|voucher.*redeem" && echo "CONCURRENCY" || true
|
|
151
151
|
echo "$FILES" | grep -cE "migration|\.sql$" && echo "DATABASE" || true
|
|
152
152
|
echo "$DIFF" | grep -cE "create_async_engine|AsyncSession|connect_args|pool_size|prepared_statement|engine_from_config|sessionmaker" && echo "DB_CONFIG" || true
|
|
153
153
|
echo "$FILES" | grep -cE "router|routes" && echo "API_DESIGN" || true
|
|
@@ -397,7 +397,7 @@ For each changed file, identify its **activation path** — how does execution r
|
|
|
397
397
|
|
|
398
398
|
Map each changed file to its activation requirements.
|
|
399
399
|
|
|
400
|
-
**Layout path resolution** — Before applying the table below, read your project's layout from `forge.yaml → review.layout` and substitute the values into the pattern column. Defaults (used when the key is absent)
|
|
400
|
+
**Layout path resolution** — Before applying the table below, read your project's layout from `forge.yaml → review.layout` and substitute the values into the pattern column. Defaults (used when the key is absent) are the ForgeDock install defaults:
|
|
401
401
|
|
|
402
402
|
| `forge.yaml` key | Default | Used in table as |
|
|
403
403
|
|-----------------|---------|-----------------|
|
|
@@ -461,6 +461,23 @@ if [ "$BASH_AVAILABLE" = "true" ]; then
|
|
|
461
461
|
# Output is structured: "BLOCKING: ...", "WARNING: ...", "OK: ..." per line.
|
|
462
462
|
|
|
463
463
|
# 1. Route/router/middleware/shared-module/component registration
|
|
464
|
+
# Export forge.yaml layout overrides so verify-route-registration.sh uses project-configured
|
|
465
|
+
# paths instead of project defaults. The script supports these env vars (lines 36-44 of
|
|
466
|
+
# verify-route-registration.sh) but requires the caller to set them. <!-- Added: forge#1349 -->
|
|
467
|
+
if [ -f "$REPO_ROOT/forge.yaml" ]; then
|
|
468
|
+
_PAGES_ROOT=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
|
|
469
|
+
| grep -E '^\s*pages:' | head -1 | sed 's/.*pages:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
470
|
+
_API_ROUTERS=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
|
|
471
|
+
| grep -E '^\s*api_routers_dir:' | head -1 | sed 's/.*api_routers_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
472
|
+
_API_MAIN=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
|
|
473
|
+
| grep -E '^\s*api_main:' | head -1 | sed 's/.*api_main:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
474
|
+
_API_MIDDLEWARE=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
|
|
475
|
+
| grep -E '^\s*api_middleware_dir:' | head -1 | sed 's/.*api_middleware_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
476
|
+
[ -n "$_PAGES_ROOT" ] && export FORGE_PAGES_ROOT="$_PAGES_ROOT"
|
|
477
|
+
[ -n "$_API_ROUTERS" ] && export FORGE_API_ROUTERS_DIR="$_API_ROUTERS"
|
|
478
|
+
[ -n "$_API_MAIN" ] && export FORGE_API_MAIN="$_API_MAIN"
|
|
479
|
+
[ -n "$_API_MIDDLEWARE" ] && export FORGE_API_MIDDLEWARE_DIR="$_API_MIDDLEWARE"
|
|
480
|
+
fi
|
|
464
481
|
echo "=== Running: verify-route-registration.sh ==="
|
|
465
482
|
bash "$FORGE_HOME/scripts/verify-route-registration.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
|
|
466
483
|
|
|
@@ -612,7 +629,7 @@ Format: `INTEG-N|CONFIRMED|HIGH|file:line|Changed code may be unreachable: {reas
|
|
|
612
629
|
DIFF=$(gh pr diff $ARGUMENTS)
|
|
613
630
|
FILES=$(gh pr diff $ARGUMENTS --name-only)
|
|
614
631
|
echo "=== RISK SIGNALS ==="
|
|
615
|
-
echo "$DIFF" | grep -cE "subprocess|exec|eval|system\(|popen|heredoc
|
|
632
|
+
echo "$DIFF" | grep -cE "subprocess|exec|eval|system\(|popen|heredoc" && echo " UNTRUSTED_INPUT_PROCESSING" || true
|
|
616
633
|
echo "$DIFF" | grep -cE "\.sh$|bash|shell|cron" && echo " SHELL_SCRIPT" || true
|
|
617
634
|
echo "$DIFF" | grep -cE "credit|balance|debit|charge|refund|stripe" && echo " FINANCIAL" || true
|
|
618
635
|
echo "$DIFF" | grep -cE "SessionUser|CurrentUser|jwt|oauth|password|token|secret|x.forwarded.for|x_forwarded_for|forwarded_for|rate.limit.*ip|ip.*rate.limit|algorithm.*HS256|algorithm.*RS256|NEXTAUTH_SECRET|JWT_SECRET|admin.proxy" && echo " AUTH_SENSITIVE" || true
|
|
@@ -624,6 +641,36 @@ echo "$DIFF" | grep -cE "subprocess|os\.system|eval\(|exec\(|pickle|yaml\.load[^
|
|
|
624
641
|
echo "$FILES" | grep -cE "^sdk/|openapi.*\.json$|openapi-versions/" && echo " SDK_OPENAPI" || true
|
|
625
642
|
```
|
|
626
643
|
|
|
644
|
+
**Churn (hot-spot) signal**: The signals above are all derived from the current diff content — none of them measure historical change frequency, which is one of the strongest empirical predictors of defect density. Compute a bounded per-file churn tier for the PR's changed files only (never repo-wide) and carry it forward as `CHURN_CONTEXT` for Phase 3C:
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
CHURN_WINDOW="90 days ago" # named constant — must match the same window used in architect.md Phase A5
|
|
648
|
+
CHURN_CONTEXT=""
|
|
649
|
+
# Herestring (not a piped `| while read`) — a pipe would run the loop body in a
|
|
650
|
+
# subshell in bash, silently discarding CHURN_CONTEXT once the loop exits. $FILES
|
|
651
|
+
# is one path per line (gh pr diff --name-only), so `read -r` per line is safe
|
|
652
|
+
# even when a path contains embedded spaces.
|
|
653
|
+
while IFS= read -r FILE; do
|
|
654
|
+
[ -z "$FILE" ] && continue
|
|
655
|
+
COMMITS=$(git log --oneline --since="$CHURN_WINDOW" -- "$FILE" 2>/dev/null | wc -l)
|
|
656
|
+
if [ "$COMMITS" -ge 15 ]; then
|
|
657
|
+
# Real newline appended via $'\n' (ANSI-C quoting), not a literal backslash-n.
|
|
658
|
+
# A literal "\n" would only be interpreted by `echo -e` — it survives
|
|
659
|
+
# unprocessed into Phase 3C's raw template substitution ([CHURN_CONTEXT] ->
|
|
660
|
+
# $CHURN_CONTEXT), which does not reprocess escapes, so with 2+ HOT files
|
|
661
|
+
# multi-hotspot PRs would render squished in the agent prompt. A real
|
|
662
|
+
# newline here makes the variable correct for every consumer.
|
|
663
|
+
CHURN_CONTEXT="${CHURN_CONTEXT}${FILE} (${COMMITS} commits in last 90 days — HOT)"$'\n'
|
|
664
|
+
fi
|
|
665
|
+
done <<< "$FILES"
|
|
666
|
+
if [ -z "$CHURN_CONTEXT" ]; then
|
|
667
|
+
CHURN_CONTEXT="No hot-spot files detected (all changed files under 15 commits in the last 90 days)."
|
|
668
|
+
fi
|
|
669
|
+
echo "$CHURN_CONTEXT"
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Tiers (fixed thresholds, identical to `architect.md` Phase A5): **HOT** = 15+ commits / 90 days, **MEDIUM** = 5–14, **LOW** = 0–4. Only HOT-tier files are listed in `CHURN_CONTEXT` — this keeps the agent prompt signal short and high-value rather than dumping a churn count for every file. This does not change agent selection (3B) — it is a scrutiny prior surfaced to whichever agents are already selected, substituted as `[CHURN_CONTEXT]` in Phase 3C.
|
|
673
|
+
|
|
627
674
|
### 3B: Select Agents
|
|
628
675
|
|
|
629
676
|
| Risk Signal | Required Agents |
|
|
@@ -687,8 +734,9 @@ This file contains the Evidence-Based Review Protocol, Structured Findings Proto
|
|
|
687
734
|
2. Substitute: `[PR_NUMBER]`, `[REVIEW_SHA]`, `[REVIEW_SHA_SHORT]`, `[TITLE]`, relevant files list
|
|
688
735
|
3. Substitute: `[PROJECT_NAME]` → `$PROJECT_NAME`, `[PROJECT_CONTEXT]` → `$PROJECT_CONTEXT`, `[TECH_STACK]` → `$TECH_STACK`
|
|
689
736
|
4. Substitute per-agent domain context: `[DOMAIN_CONTEXT]` → the agent's matching key from `forge.yaml → review.domains` (e.g., `$DOMAIN_CONTEXT_AUTH` for the auth agent, `$DOMAIN_CONTEXT_BILLING` for the billing agent)
|
|
690
|
-
5.
|
|
691
|
-
6.
|
|
737
|
+
5. Substitute the shared hot-spot prior: `[CHURN_CONTEXT]` → `$CHURN_CONTEXT` (computed once in Phase 3A, same value for every agent — this is a PR-level fact, not a per-domain config value, so it is NOT read from `forge.yaml`)
|
|
738
|
+
6. If Phase 2.5 found broken assumptions, append them to the agent's prompt as "Pre-found integration issues to verify"
|
|
739
|
+
7. Launch via `Task` tool with the resolved model (default `"sonnet"`, fallback `"opus"` if rate-limited)
|
|
692
740
|
|
|
693
741
|
**CRITICAL**: Launch ALL selected agents in a SINGLE message using multiple Task tool calls. Each agent posts findings directly to the PR via `gh pr comment`.
|
|
694
742
|
|
|
@@ -1062,7 +1110,7 @@ else
|
|
|
1062
1110
|
fi
|
|
1063
1111
|
```
|
|
1064
1112
|
|
|
1065
|
-
**Important**: Phase 8 ONLY merges the PR. It does NOT close the issue, update labels, or clean up worktrees. When invoked via `/work-on`, those responsibilities belong to `work-on/close.md`
|
|
1113
|
+
**Important**: Phase 8 ONLY merges the PR. It does NOT close the issue, update labels, or clean up worktrees. When invoked via `/work-on`, those responsibilities belong to `work-on/close.md` (work-on.md Phase 6 — triggered when PR is merged and issue is still open). Doing them here would cause the issue to be closed with `workflow:merged` before Phase 6 runs, skipping the close phase entirely.
|
|
1066
1114
|
|
|
1067
1115
|
### 8B: Post-Merge Review Finding Demilestoning (Milestone PRs Only)
|
|
1068
1116
|
|
package/commands/scope.md
CHANGED
|
@@ -79,13 +79,21 @@ Limit `DOMAIN_TERMS` to the 10 most specific (skip generic words: "fix", "bug",
|
|
|
79
79
|
Search `REPO_PATH` for each keyword to find candidate files:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
+
# {TARGET_FILES} and {DOMAIN_TERMS} are space-separated arguments (same contract
|
|
83
|
+
# as {AFFECTED_FILES} elsewhere) — split explicitly on IFS=' ' into arrays instead
|
|
84
|
+
# of a bare `for x in {PLACEHOLDER}`, which word-splits on the shell's default IFS
|
|
85
|
+
# (space, tab, AND newline) and would corrupt any file path or compound domain
|
|
86
|
+
# term containing a space.
|
|
87
|
+
IFS=' ' read -ra TARGET_FILES_ARR <<< "{TARGET_FILES}"
|
|
88
|
+
IFS=' ' read -ra DOMAIN_TERMS_ARR <<< "{DOMAIN_TERMS}"
|
|
89
|
+
|
|
82
90
|
# For each file explicitly named in the issue
|
|
83
|
-
for file in {
|
|
91
|
+
for file in "${TARGET_FILES_ARR[@]}"; do
|
|
84
92
|
find "$REPO_PATH" -name "$file" -not -path "*/.git/*" -not -path "*/node_modules/*" 2>/dev/null
|
|
85
93
|
done
|
|
86
94
|
|
|
87
95
|
# For each domain term: grep for references
|
|
88
|
-
for term in {
|
|
96
|
+
for term in "${DOMAIN_TERMS_ARR[@]}"; do
|
|
89
97
|
grep -rl "$term" "$REPO_PATH" \
|
|
90
98
|
--include="*.py" --include="*.ts" --include="*.tsx" \
|
|
91
99
|
--include="*.md" --include="*.yml" --include="*.yaml" \
|
|
@@ -96,7 +104,7 @@ done
|
|
|
96
104
|
|
|
97
105
|
# Git log — files recently changed alongside the named files
|
|
98
106
|
if [ -n "{TARGET_FILES}" ]; then
|
|
99
|
-
for file in {
|
|
107
|
+
for file in "${TARGET_FILES_ARR[@]}"; do
|
|
100
108
|
git -C "$REPO_PATH" log --oneline -20 -- "$file" \
|
|
101
109
|
--name-only --format="" 2>/dev/null | grep -v "^$" | sort -u | head -10
|
|
102
110
|
done
|
|
@@ -110,13 +110,14 @@ fi
|
|
|
110
110
|
**Check**:
|
|
111
111
|
```bash
|
|
112
112
|
DOCKER_COMPOSE_FILES=$(find "$REPO_PATH" -name "docker-compose*.yml" -not -path "*/.git/*" 2>/dev/null)
|
|
113
|
-
|
|
113
|
+
while IFS= read -r f; do
|
|
114
|
+
[ -z "$f" ] && continue
|
|
114
115
|
HITS=$(grep -n '0\.0\.0\.0:[0-9]' "$f" 2>/dev/null | grep -v '^\s*#' || true)
|
|
115
116
|
if [ -n "$HITS" ]; then
|
|
116
117
|
echo "FINDING [1A] $f:"
|
|
117
118
|
echo "$HITS"
|
|
118
119
|
fi
|
|
119
|
-
done
|
|
120
|
+
done <<< "$DOCKER_COMPOSE_FILES"
|
|
120
121
|
```
|
|
121
122
|
|
|
122
123
|
**Confirm**: For each hit, verify the port is NOT a web-facing port (80, 443, 8080, 3000). Ports like 6432 (PgBouncer), 6379 (Redis), 5432 (Postgres), 27017 (MongoDB) bound to `0.0.0.0` are confirmed findings.
|
|
@@ -134,12 +135,13 @@ done
|
|
|
134
135
|
**Check**:
|
|
135
136
|
```bash
|
|
136
137
|
DOCKERFILES=$(find "$REPO_PATH" -name "Dockerfile*" -not -path "*/.git/*" 2>/dev/null)
|
|
137
|
-
|
|
138
|
+
while IFS= read -r f; do
|
|
139
|
+
[ -z "$f" ] && continue
|
|
138
140
|
# Skip if USER directive is present (anywhere in the file, case-insensitive)
|
|
139
141
|
if ! grep -iq '^\s*USER\s' "$f" 2>/dev/null; then
|
|
140
142
|
echo "FINDING [1B] $f: no USER directive — container runs as root"
|
|
141
143
|
fi
|
|
142
|
-
done
|
|
144
|
+
done <<< "$DOCKERFILES"
|
|
143
145
|
```
|
|
144
146
|
|
|
145
147
|
**Confirm**: Check if the Dockerfile is for a long-running service (not a one-shot build image). If service container with no USER → confirmed finding.
|
|
@@ -157,14 +159,15 @@ done
|
|
|
157
159
|
**Check**:
|
|
158
160
|
```bash
|
|
159
161
|
DOCKER_COMPOSE_FILES=$(find "$REPO_PATH" -name "docker-compose*.yml" -not -path "*/.git/*" 2>/dev/null)
|
|
160
|
-
|
|
162
|
+
while IFS= read -r f; do
|
|
163
|
+
[ -z "$f" ] && continue
|
|
161
164
|
# Redis without requirepass
|
|
162
165
|
REDIS_NO_AUTH=$(grep -A5 'image:.*redis' "$f" 2>/dev/null | grep -v 'requirepass\|password' | grep -c 'redis' || true)
|
|
163
166
|
# Postgres without POSTGRES_PASSWORD
|
|
164
167
|
PG_NO_AUTH=$(awk '/image:.*postgres/{found=1} found && /POSTGRES_PASSWORD/{found=0} found && /^[[:space:]]*-[[:space:]]*[A-Z]/{print}' "$f" 2>/dev/null | grep -v 'POSTGRES_PASSWORD' | head -3 || true)
|
|
165
168
|
|
|
166
169
|
[ -n "$PG_NO_AUTH" ] && echo "FINDING [1C] $f: Postgres service may lack POSTGRES_PASSWORD"
|
|
167
|
-
done
|
|
170
|
+
done <<< "$DOCKER_COMPOSE_FILES"
|
|
168
171
|
```
|
|
169
172
|
|
|
170
173
|
**Manual review**: Check for `POSTGRES_PASSWORD` and `requirepass` in compose environment sections. Flag any service using a blank/trivial password as a documented value.
|
|
@@ -184,7 +187,8 @@ done
|
|
|
184
187
|
**Check**:
|
|
185
188
|
```bash
|
|
186
189
|
WORKFLOW_FILES=$(find "$REPO_PATH/.github/workflows" -name "*.yml" 2>/dev/null)
|
|
187
|
-
|
|
190
|
+
while IFS= read -r f; do
|
|
191
|
+
[ -z "$f" ] && continue
|
|
188
192
|
# Find security scanner steps then check if continue-on-error follows within 5 lines
|
|
189
193
|
awk '
|
|
190
194
|
/trivy|snyk|owasp|dependency.check|gitleaks|semgrep|gosec|bandit/ {
|
|
@@ -195,7 +199,7 @@ for f in $WORKFLOW_FILES; do
|
|
|
195
199
|
print FILENAME ":" NR ": continue-on-error: true near security scanner at line " scanner_line
|
|
196
200
|
}
|
|
197
201
|
' FILENAME="$f" "$f" 2>/dev/null || true
|
|
198
|
-
done
|
|
202
|
+
done <<< "$WORKFLOW_FILES"
|
|
199
203
|
```
|
|
200
204
|
|
|
201
205
|
**Confirm**: Verify the flagged step is indeed a security scanner step (not a notification or deploy step). Confirmed if scanner produces failure exit codes that are being swallowed.
|
|
@@ -213,10 +217,11 @@ done
|
|
|
213
217
|
**Check**:
|
|
214
218
|
```bash
|
|
215
219
|
CONFIG_FILES=$(find "$REPO_PATH/infra" "$REPO_PATH/services" -name "*.conf" -o -name "*.yml" -o -name "nginx.conf" 2>/dev/null | grep -v '.git')
|
|
216
|
-
|
|
220
|
+
while IFS= read -r f; do
|
|
221
|
+
[ -z "$f" ] && continue
|
|
217
222
|
HITS=$(grep -n '^\s*#.*\(deny\|allow\s\|auth_basic\|auth_request\|satisfy all\|limit_req\|access_by_lua\)' "$f" 2>/dev/null || true)
|
|
218
223
|
[ -n "$HITS" ] && echo "FINDING [2B] $f:" && echo "$HITS"
|
|
219
|
-
done
|
|
224
|
+
done <<< "$CONFIG_FILES"
|
|
220
225
|
```
|
|
221
226
|
|
|
222
227
|
**Confirm**: Distinguish intentional comments (documentation) from disabled access control rules. Confirmed if the commented line is a functional directive that would restrict access.
|
|
@@ -236,10 +241,11 @@ done
|
|
|
236
241
|
**Check**:
|
|
237
242
|
```bash
|
|
238
243
|
CSP_FILES=$(grep -rl "Content-Security-Policy\|script-src" "$REPO_PATH" --include="*.js" --include="*.ts" --include="*.json" --include="*.conf" --include="*.yml" 2>/dev/null | grep -v '.git' | grep -v node_modules)
|
|
239
|
-
|
|
244
|
+
while IFS= read -r f; do
|
|
245
|
+
[ -z "$f" ] && continue
|
|
240
246
|
HITS=$(grep -n "script-src.*unsafe-inline\|unsafe-inline.*script-src" "$f" 2>/dev/null || true)
|
|
241
247
|
[ -n "$HITS" ] && echo "FINDING [3A] $f:" && echo "$HITS"
|
|
242
|
-
done
|
|
248
|
+
done <<< "$CSP_FILES"
|
|
243
249
|
```
|
|
244
250
|
|
|
245
251
|
**Confirm**: Verify the CSP header applies to the production site (not just dev/storybook). Check if a nonce-based approach is already in use (nonce + unsafe-inline is still a finding but lower severity).
|
|
@@ -257,10 +263,11 @@ done
|
|
|
257
263
|
**Check**:
|
|
258
264
|
```bash
|
|
259
265
|
HEADER_FILES=$(grep -rl "X-App-Version\|X-Deployment-Color\|X-Git-Sha\|X-Build-Id\|Server:" "$REPO_PATH" --include="*.py" --include="*.ts" --include="*.js" --include="*.conf" 2>/dev/null | grep -v '.git' | grep -v node_modules)
|
|
260
|
-
|
|
266
|
+
while IFS= read -r f; do
|
|
267
|
+
[ -z "$f" ] && continue
|
|
261
268
|
HITS=$(grep -n '"X-App-Version"\|"X-Deployment-Color"\|"X-Git-Sha"\|"X-Build-Id"\|"Server:' "$f" 2>/dev/null || true)
|
|
262
269
|
[ -n "$HITS" ] && echo "FINDING [3B] $f:" && echo "$HITS"
|
|
263
|
-
done
|
|
270
|
+
done <<< "$HEADER_FILES"
|
|
264
271
|
```
|
|
265
272
|
|
|
266
273
|
**Confirm**: Check if the header is set in a response (not just read). If set in outbound responses → confirmed finding. Internal-only APIs (not publicly routed) are `priority:P3`; public APIs are `priority:P2`.
|
|
@@ -435,14 +442,15 @@ payment_intent.payment_failed
|
|
|
435
442
|
**Check**:
|
|
436
443
|
```bash
|
|
437
444
|
WEBHOOK_FILES=$(grep -rl "stripe\|webhook" "$REPO_PATH/services" --include="*.py" 2>/dev/null | grep -v '.git' | head -10)
|
|
438
|
-
|
|
445
|
+
while IFS= read -r f; do
|
|
446
|
+
[ -z "$f" ] && continue
|
|
439
447
|
REQUIRED_EVENTS="customer.subscription.deleted invoice.payment_failed charge.refunded charge.dispute.created"
|
|
440
448
|
for event in $REQUIRED_EVENTS; do
|
|
441
449
|
if ! grep -q "\"$event\"\|'$event'" "$f" 2>/dev/null; then
|
|
442
450
|
echo "FINDING [4A] $f: Stripe event '$event' not handled"
|
|
443
451
|
fi
|
|
444
452
|
done
|
|
445
|
-
done
|
|
453
|
+
done <<< "$WEBHOOK_FILES"
|
|
446
454
|
```
|
|
447
455
|
|
|
448
456
|
**Confirm**: Verify the event type appears in a handler dispatch (not just a comment). Confirmed if the event would change user entitlements or financial state but has no handler.
|
|
@@ -444,7 +444,7 @@ npm version {minor|patch|major} --no-git-tag-version
|
|
|
444
444
|
# 3. Commit the version bump
|
|
445
445
|
NEW_VERSION=$(grep '"version"' package.json | grep -oP '\d+\.\d+\.\d+')
|
|
446
446
|
git add package.json package-lock.json
|
|
447
|
-
git commit -m "chore: bump version to ${NEW_VERSION}"
|
|
447
|
+
git commit -s -m "chore: bump version to ${NEW_VERSION}"
|
|
448
448
|
git push origin main
|
|
449
449
|
|
|
450
450
|
# 4. Create GitHub Release (triggers {PUBLISH_WORKFLOW} automatically)
|
|
@@ -495,7 +495,7 @@ cd {SUBPATH} && npm version {minor|patch} --no-git-tag-version && cd -
|
|
|
495
495
|
|
|
496
496
|
# Commit and push
|
|
497
497
|
git add {SUBPATH}/pyproject.toml {SUBPATH}/package.json {SUBPATH}/package-lock.json
|
|
498
|
-
git commit -m "chore({COMPONENT}): bump version to {NEW_VERSION}"
|
|
498
|
+
git commit -s -m "chore({COMPONENT}): bump version to {NEW_VERSION}"
|
|
499
499
|
git push origin fix/{COMPONENT}-version-bump
|
|
500
500
|
|
|
501
501
|
# Create PR to {STAGING_BRANCH}, merge, then trigger publish from main
|