test(15-04): add fail-closed Phase 15 gate and ASVS L1 review
scripts/check-phase15.sh --all refuses skip/no-tests/race/dirty PHP pin; the review closes T-15-01..15 and T-15-SC with executed TestNames. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
201
.planning/phases/15-journal-plugin/15-SECURITY-REVIEW.md
Normal file
201
.planning/phases/15-journal-plugin/15-SECURITY-REVIEW.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
---
|
||||||
|
phase: 15
|
||||||
|
slug: journal-plugin
|
||||||
|
status: verified
|
||||||
|
threats_total: 16
|
||||||
|
threats_closed: 16
|
||||||
|
threats_open: 0
|
||||||
|
accepted_risks: 0
|
||||||
|
asvs_level: 1
|
||||||
|
block_on: high
|
||||||
|
created: 2026-10-06
|
||||||
|
verified: 2026-10-06
|
||||||
|
reviewer: gsd-executor (15-04 Task 3, self-performed -- see Reviewer Note)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 15 — Security Review
|
||||||
|
|
||||||
|
> Lean Journal plugin (`sm-journal-plugin`) and the proof-host boot of
|
||||||
|
> user+translate+journal. Every Phase 15 threat locked in plans 01–04 is
|
||||||
|
> mapped below to executed, named Go evidence. Unmapped IDs would be a
|
||||||
|
> review gap, not an accepted risk; none exist.
|
||||||
|
|
||||||
|
**Date:** 2026-10-06
|
||||||
|
**Scope:** Plans 15-01 through 15-04; `sm-journal-plugin`; proof host
|
||||||
|
`sm-grzybyfunkcjonalne-app`; `scripts/check-phase15.sh`.
|
||||||
|
**Repos grepped:** `sm-journal-plugin`, `summercms.go` (excluding
|
||||||
|
`.planning/` except this review), `sm-grzybyfunkcjonalne-app`.
|
||||||
|
|
||||||
|
## Reviewer Note
|
||||||
|
|
||||||
|
15-04-PLAN.md Task 3 calls for an independent `gsd-security-auditor`
|
||||||
|
agent pass. This Cursor session has no dedicated security-auditor
|
||||||
|
subagent (same fallback as 14.2.1-04): the 15-04 executor performed the
|
||||||
|
review directly. Every high threat below is closed with source citations
|
||||||
|
and named tests **re-executed during this review** (2026-10-06 plugin
|
||||||
|
`go test ./... -race` and host `go test ./... -race`), not merely
|
||||||
|
inherited from earlier plans.
|
||||||
|
|
||||||
|
No external API integration: this phase ports a compiled plugin and local
|
||||||
|
host contracts only. No external SaaS SDK this phase (Typesense stays
|
||||||
|
behind a default-off gate; TestSearchGateOff recorded zero HTTP).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verdict Summary
|
||||||
|
|
||||||
|
The register contains **16 total threats: 16 closed, 0 open, 0 accepted
|
||||||
|
risks**. High findings block phase completion; all high rows are mitigate
|
||||||
|
with executed named tests. PHP pin SHA `02110eb1c0c3861370b0b9b47b209a0702ac5d88`
|
||||||
|
is unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description | Data Crossing |
|
||||||
|
|----------|-------------|----------------|
|
||||||
|
| anonymous GET → published posts | public `/_journal/api/v1` | published rows only; drafts 404 without `data` |
|
||||||
|
| backend JWT → writes / media | HS256 `aud=backend` | title/content/files; never frontend audience |
|
||||||
|
| Fillable / API assigns → GORM | untrusted JSON | nest_*, redactor_id, user_id must not persist from maps |
|
||||||
|
| markdown → stored HTML | FormatHTML rejectUnsafe | script/iframe/event/js schemes |
|
||||||
|
| test fixture → production binary | process-local plugins | must not appear in host `plugins.gen.go` |
|
||||||
|
| gate → production claims | skipped containers / dirty PHP | named PASS + final marker |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Severity | Disposition | Proof |
|
||||||
|
|-----------|----------|-----------|----------|-------------|-------|
|
||||||
|
| T-15-01 | Spoofing | POST `/_journal/api/v1/posts` | high | mitigate | `journal_api_writes_test.go:TestJournalWriteUnauthenticated`; frontend audience 401 |
|
||||||
|
| T-15-02 | Information Disclosure | GET posts/{slug} drafts | high | mitigate | `TestJournal005DraftShow` 404 without `data`; owner/`access_other_posts` 200 |
|
||||||
|
| T-15-03 | Tampering | POST `/media/upload` | high | mitigate | `TestJournal006MediaUpload` 403 without `access_posts`; folder `..` 422; `/journal/` prefix |
|
||||||
|
| T-15-04 | Elevation of Privilege | Category/Tag/Post Fillable | high | mitigate | `models/fillable_test.go:TestFillable`; `TestJournalAPIMassAssignRedactor` |
|
||||||
|
| T-15-05 | Tampering | gormigrate DDL | high | mitigate | `TestJournalTables`; `TestJournalMigrationsRollbackAndRemigrate`; no AutoMigrate |
|
||||||
|
| T-15-06 | Tampering | MorphName | high | mitigate | `TestTranslatable`; `TestPostTranslatableSmoke` PHP class strings |
|
||||||
|
| T-15-07 | Elevation of Privilege | Posts admin | high | mitigate | `TestPostsAdminForbidden` 403 without `access_posts`; owner scope in Plan 02 |
|
||||||
|
| T-15-08 | Tampering | FormatHTML | high | mitigate | `classes/format_html_test.go:TestFormatHTMLRejectsUnsafeHTML` |
|
||||||
|
| T-15-09 | Elevation of Privilege | access_publish | high | mitigate | `TestJournalWriteUnauthenticated` publish 403; `TestPostsAdminCreateSmoke/publish_without_access_publish` |
|
||||||
|
| T-15-10 | Spoofing | write API tokens | high | mitigate | `TestJournalWriteUnauthenticated` / `TestJournalWriteFrontendAudience` reject `aud=user` |
|
||||||
|
| T-15-11 | Information Disclosure | Typesense sync | high | mitigate | `search_test.go:TestSearchGateOff` zero HTTP; unpublished `ShouldBeSearchable` false with gate flipped |
|
||||||
|
| T-15-12 | Denial of Service | X-Forwarded-For | medium | mitigate | `plugin.go` buckets use `surf.ClientIP` + `TrustedProxies`; `TestJournalBuckets` |
|
||||||
|
| T-15-13 | Tampering | error envelope | high | mitigate | `TestJournalWriteUnauthenticated` PHP `{error}` string, no cabana admin envelope |
|
||||||
|
| T-15-14 | Repudiation | phase gate | high | mitigate | `scripts/check-phase15.sh` detector refuses skip/no-tests/race; `--self-test` |
|
||||||
|
| T-15-15 | Information Disclosure | unpublished title prefix | medium | mitigate | `TestJournalAPIShowNeighbors` JSON title omits `UnpublishedTitlePrefix` |
|
||||||
|
| T-15-SC | Tampering | package installs | high | mitigate | plugin `replace` is only `summercms => ../summercms.go`; goldmark already in the graph; no new SaaS SDK |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings by Threat
|
||||||
|
|
||||||
|
### T-15-01 — unauthenticated and frontend-audience writes
|
||||||
|
|
||||||
|
- **Source:** `controllers/api/auth.go` `requireBackendPrincipal`; PHP `{error:"Authentication required"}`.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestJournalWriteUnauthenticated` PASS; `TestJournalWriteFrontendAudience` PASS; featured-image POST/DELETE 401 in `TestJournalFeaturedImageUnauthenticated` PASS.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-02 — draft enumeration
|
||||||
|
|
||||||
|
- **Source:** `controllers/api/posts.go` Show; 404 without `data` unless owner or `access_other_posts`.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestJournal005DraftShow` PASS; `TestJournalEndToEnd` anonymous draft 404 PASS.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-03 — media traversal
|
||||||
|
|
||||||
|
- **Source:** `controllers/api/media.go` folder regex, `..` reject, forced `/journal/` prefix.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestJournal006MediaUpload` PASS; `TestMediaObjectPath` PASS.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-04 — mass assignment
|
||||||
|
|
||||||
|
- **Source:** Tag/Category `Fillable`; Post API `buildNewPost` field-by-field (never `lagoon.Fill` of `redactor_id`/`user_id`).
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestFillable` PASS; `TestJournalAPIMassAssignRedactor` PASS.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-05 — schema / AutoMigrate
|
||||||
|
|
||||||
|
- **Source:** gormigrate IDs `202610060001`–`007`; production plugin has no `AutoMigrate(`.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestJournalTables` PASS; `TestJournalMigrationsRollbackAndRemigrate` PASS. Gate `--forbidden` refuses production AutoMigrate.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-06 — MorphName
|
||||||
|
|
||||||
|
- **Source:** hard-coded `Golem15\Journal\Models\Post` / `Category` / `Tag`.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestTranslatable` PASS; `TestPostTranslatableSmoke` PASS.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-07 — admin access_posts
|
||||||
|
|
||||||
|
- **Source:** Posts controller `RequiredPermissions`; List/FormExtendQuery owner scope without `access_other_posts`.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestPostsAdminForbidden` PASS (403 without grant).
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-08 — stored XSS in content_html
|
||||||
|
|
||||||
|
- **Source:** `classes/format_html.go` goldmark without unsafe HTML; `rejectUnsafe` for script/iframe/event/js/vbscript/data.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestFormatHTMLRejectsUnsafeHTML` and subtests script/iframe/event/javascript/vbscript/data PASS.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-09 — publish permission
|
||||||
|
|
||||||
|
- **Source:** Store/Update refuse `published` without `golem15.journal.access_publish`; admin `ForbiddenError`.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestJournalWriteUnauthenticated` publish 403 PASS; `TestPostsAdminCreateSmoke/publish_without_access_publish` PASS.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-10 — frontend token on writes
|
||||||
|
|
||||||
|
- **Source:** backend JWT audience only; no Apparatus personal tokens.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestJournalWriteUnauthenticated` frontend-audience POST 401 PASS.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-11 — Typesense leak
|
||||||
|
|
||||||
|
- **Source:** `search_use_typesense` default false; `ShouldBeSearchable` false when unpublished or gate off.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestSearchGateOff` PASS (zero HTTP; must not skip).
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-12 — rate-limit XFF
|
||||||
|
|
||||||
|
- **Source:** `Plugin.Buckets` keys `surf.ClientIP` with `TrustedProxies`.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestJournalBuckets` PASS.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-13 — envelope mixup
|
||||||
|
|
||||||
|
- **Source:** journal `writeAPIError` PHP `{error}` string; must not use cabana admin `{error:{code}}` on public API.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestJournalWriteUnauthenticated` PASS (string error, no `data`).
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-14 — gate repudiation
|
||||||
|
|
||||||
|
- **Source:** `scripts/check-phase15.sh` JSON detector.
|
||||||
|
- **Test evidence:** `--self-test` (fail/skip/zero/no-tests/race/missing-named) executed as the first `--all` stage.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-15 — unpublished lock prefix
|
||||||
|
|
||||||
|
- **Source:** API serialize uses raw `Title`; `console.UnpublishedTitlePrefix` is import-only.
|
||||||
|
- **Test evidence (re-run 2026-10-06):** `TestJournalAPIShowNeighbors` PASS.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
### T-15-SC — package installs
|
||||||
|
|
||||||
|
- **Source:** plugin `go.mod` replace of summercms only; goldmark v1.8.6 already required for FormatHTML.
|
||||||
|
- **Test evidence:** `--layout` replace check; no `go get` of a new SaaS SDK this plan.
|
||||||
|
- **Disposition:** closed / mitigate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Submodule provenance
|
||||||
|
|
||||||
|
Host gitlinks `plugins/golem15/{user,translate,journal}` are mode `160000`.
|
||||||
|
`TestBootUserTranslateJournal` PASS (re-run 2026-10-06). `--layout` requires
|
||||||
|
the three production IDs and CORS `_journal/api/*`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API-coverage declaration
|
||||||
|
|
||||||
|
No external SaaS SDK this phase. Typesense is optional and default-off;
|
||||||
|
`TestSearchGateOff` observed zero outbound HTTP.
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
phase: "15"
|
phase: "15"
|
||||||
slug: "journal-plugin"
|
slug: "journal-plugin"
|
||||||
status: draft
|
status: validated
|
||||||
nyquist_compliant: false
|
nyquist_compliant: true
|
||||||
wave_0_complete: false
|
wave_0_complete: true
|
||||||
created: "2026-10-06"
|
created: "2026-10-06"
|
||||||
|
validated: "2026-10-06"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Phase 15 — Validation Strategy
|
# Phase 15 — Validation Strategy
|
||||||
@@ -20,8 +21,8 @@ created: "2026-10-06"
|
|||||||
| **Framework** | Go `testing` + testcontainers-go v0.44.0 (Postgres) |
|
| **Framework** | Go `testing` + testcontainers-go v0.44.0 (Postgres) |
|
||||||
| **Config file** | none — `go test ./...` |
|
| **Config file** | none — `go test ./...` |
|
||||||
| **Quick run command** | `go test ./... -short -count=1` in `sm-journal-plugin` + host `go test ./... -short -count=1` |
|
| **Quick run command** | `go test ./... -short -count=1` in `sm-journal-plugin` + host `go test ./... -short -count=1` |
|
||||||
| **Full suite command** | `go test ./... -count=1` in `sm-journal-plugin`, host, and `summercms.go` only if framework files change |
|
| **Full suite command** | `go test ./... -count=1` in `sm-journal-plugin` and `sm-grzybyfunkcjonalne-app`; `bash scripts/check-phase15.sh --all` |
|
||||||
| **Estimated runtime** | ~60 seconds (short); longer with Postgres + race on last plan |
|
| **Estimated runtime** | ~60 seconds (short); ~2 minutes with Postgres + race on last plan |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -38,19 +39,24 @@ created: "2026-10-06"
|
|||||||
|
|
||||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||||
| 15-W0-01 | 01 | 0 | D-01 | — | Tables `golem15_journal_posts\|categories\|tags` after migrate | integration | `go test ./updates -run TestJournalTables -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-01 | 01 | 0 | D-01 | T-15-05 | Tables `golem15_journal_posts\|categories\|tags` after migrate | integration | `go test ./updates -run TestJournalTables -count=1` | ✅ `updates/postgres_test.go` | ✅ green |
|
||||||
| 15-W0-02 | 02 | 0 | D-10 | — | Post Translatable list + MorphName PHP string | unit | `go test ./models -run TestPostTranslatable -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-02 | 02 | 0 | D-10 | T-15-06 | Post Translatable list + MorphName PHP string | unit | `go test ./models -run TestTranslatable -count=1` | ✅ `models/translatable_test.go` | ✅ green |
|
||||||
| 15-W0-03 | 02 | 0 | D-11 | — | `type: mlmarkdown` compiles on posts form | unit | `go test ./... -run TestPostsFormCompiles -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-03 | 02 | 0 | D-11 | — | `type: mlmarkdown` compiles on posts form | unit | `go test ./... -run TestPostsFormCompiles -count=1` | ✅ `posts_admin_smoke_test.go` | ✅ green |
|
||||||
| 15-W0-04 | 03 | 0 | D-12 | T-15-SC | Gate off → zero search HTTP on Post save | unit | `go test ./... -run TestSearchGateOff -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-04 | 03 | 0 | D-12 | T-15-11 | Gate off → zero search HTTP on Post save | unit | `go test ./... -run TestSearchGateOff -count=1` | ✅ `search_test.go` | ✅ green |
|
||||||
| 15-W0-05 | 02 | 0 | D-13 | — | Commands registered `journal:export-posts` / `journal:import-posts` | unit | `go test ./... -run TestJournalCommands -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-05 | 02 | 0 | D-13 | — | Commands registered `journal:export-posts` / `journal:import-posts` | unit | `go test ./... -run TestJournalCommands -count=1` | ✅ `plugin_test.go` | ✅ green |
|
||||||
| 15-W0-06 | 03 | 0 | D-14 | — | GET `/_journal/api/v1/posts` anonymous 200 | integration | `go test ./... -run TestJournalPublicList -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-06 | 03 | 0 | D-14 | — | GET `/_journal/api/v1/posts` anonymous 200 | integration | `go test ./... -run TestJournalPublicList -count=1` | ✅ `journal_public_list_smoke_test.go` | ✅ green |
|
||||||
| 15-W0-07 | 03 | 0 | D-15 | T-15-01 | POST posts without Bearer 401 `Authentication required` | integration | `go test ./... -run TestJournalWriteUnauthenticated -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-07 | 03 | 0 | D-15 | T-15-01 | POST posts without Bearer 401 `Authentication required` | integration | `go test ./... -run TestJournalWriteUnauthenticated -count=1` | ✅ `journal_api_writes_test.go` | ✅ green |
|
||||||
| 15-W0-08 | 03 | 0 | D-17 | — | Buckets named `journal-public-api` and `journal-api` Max 120 | unit | `go test ./... -run TestJournalBuckets -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-08 | 03 | 0 | D-17 | T-15-12 | Buckets named `journal-public-api` and `journal-api` Max 120 | unit | `go test ./... -run TestJournalBuckets -count=1` | ✅ `plugin_test.go` | ✅ green |
|
||||||
| 15-W0-09 | 01 | 0 | D-17 | — | Host CORS includes `_journal/api/*` | unit | host `go test ./... -run TestCORS -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-09 | 01 | 0 | D-17 | — | Host CORS includes `_journal/api/*` | unit | host `go test ./... -run TestCORS -count=1` | ✅ host `boot_test.go` | ✅ green |
|
||||||
| 15-W0-10 | 01 | 0 | D-19 | — | Host Activate 3 plugins including `golem15.journal` | smoke | host `go test ./... -run TestBootUserTranslateJournal -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-10 | 01 | 0 | D-19 | — | Host Activate 3 plugins including `golem15.journal` | smoke | host `go test ./... -run TestBootUserTranslateJournal -count=1` | ✅ host `boot_test.go` | ✅ green |
|
||||||
| 15-W0-11 | 04 | 0 | JOURNAL-005 | T-15-02 | Draft show 404 to stranger | integration | `go test ./... -run TestJournal005DraftShow -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-11 | 04 | 0 | JOURNAL-005 | T-15-02 | Draft show 404 to stranger | integration | `go test ./... -run TestJournal005DraftShow -count=1` | ✅ `journal_api_writes_test.go` | ✅ green |
|
||||||
| 15-W0-12 | 04 | 0 | JOURNAL-006 | T-15-03 | Media upload 403 without `access_posts`; path prefix | integration | `go test ./... -run TestJournal006MediaUpload -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-12 | 04 | 0 | JOURNAL-006 | T-15-03 | Media upload 403 without `access_posts`; path prefix | integration | `go test ./... -run TestJournal006MediaUpload -count=1` | ✅ `journal_api_task3_test.go` | ✅ green |
|
||||||
| 15-W0-13 | 04 | 0 | JOURNAL-001/002 | T-15-04 | Tag/Category fillable | unit | `go test ./models -run TestFillable -count=1` | ❌ Wave 0 | ⬜ pending |
|
| 15-W0-13 | 04 | 0 | JOURNAL-001/002 | T-15-04 | Tag/Category fillable | unit | `go test ./models -run TestFillable -count=1` | ✅ `models/fillable_test.go` | ✅ green |
|
||||||
|
| 15-W0-14 | 04 | 0 | D-14 | — | GET categories/tags `{data}` lists; RSS 2.0 | integration | `go test ./... -run 'TestJournalPublicCategories|TestJournalPublicTags|TestJournalRSS' -count=1` | ✅ `journal_api_writes_test.go`, `journal_api_task3_test.go` | ✅ green |
|
||||||
|
| 15-W0-15 | 04 | 0 | D-15 | T-15-01 | Featured-image POST/DELETE 401 | integration | `go test ./... -run TestJournalFeaturedImageUnauthenticated -count=1` | ✅ `journal_api_writes_test.go` | ✅ green |
|
||||||
|
| 15-W0-16 | 04 | 0 | JOURNAL-003/004 | T-15-08 | FormatHTML rejects unsafe tags | unit | `go test ./classes -run TestFormatHTMLRejectsUnsafeHTML -count=1` | ✅ `classes/format_html_test.go` | ✅ green |
|
||||||
|
| 15-W0-17 | 04 | 0 | D-04 | T-15-05 | Migrate, rollback, remigrate | integration | `go test ./updates -run TestJournalMigrationsRollbackAndRemigrate -count=1` | ✅ `updates/migrations_test.go` | ✅ green |
|
||||||
|
| 15-W0-18 | 04 | 0 | D-07/D-16 | T-15-14 | Phase gate fail-closed | other | `bash scripts/check-phase15.sh --all` | ✅ `scripts/check-phase15.sh` | ✅ green |
|
||||||
|
|
||||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||||
|
|
||||||
@@ -58,30 +64,30 @@ created: "2026-10-06"
|
|||||||
|
|
||||||
## Wave 0 Requirements
|
## Wave 0 Requirements
|
||||||
|
|
||||||
- [ ] `sm-journal-plugin` module and all test files listed above
|
- [x] `sm-journal-plugin` module and all test files listed above
|
||||||
- [ ] Host boot_test updated for three plugins + CORS
|
- [x] Host boot_test updated for three plugins + CORS
|
||||||
- [ ] Plugin Postgres harness (copy translate/user TestMain / testcontainers)
|
- [x] Plugin Postgres harness (copy translate/user TestMain / testcontainers)
|
||||||
- [ ] No new test framework install
|
- [x] No new test framework install
|
||||||
- [ ] PHPUnit XSS template tests deferred to Phase 16; FormatHTML unsafe-tag tests substitute this phase
|
- [x] PHPUnit XSS template tests deferred to Phase 16; FormatHTML unsafe-tag tests substitute this phase
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Manual-Only Verifications
|
## Manual-Only Verifications
|
||||||
|
|
||||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
| Behavior | Requirement | Why Manual | Test Instructions | Status |
|
||||||
|----------|-------------|------------|-------------------|
|
|----------|-------------|------------|-------------------|--------|
|
||||||
| Gitea remotes exist | D-18, D-22 | Requires network + credentials already used | Confirm `git ls-remote git@git.golem15.com:golem15/sm-journal-plugin.git` and the proof-host remote |
|
| Gitea remotes exist | D-18, D-22 | Requires network + credentials already used | Confirm `git ls-remote git@git.golem15.com:golem15/sm-journal-plugin.git` and the proof-host remote | ⬜ end-of-phase |
|
||||||
| Posts/Categories/Tags admin in the proof-host SPA | D-19, SC-1 | Needs running host + admin login | Boot `sm-grzybyfunkcjonalne-app`; sign in as an administrator holding `golem15.journal.*`; open Journal nav; list/create/edit a post with `mlmarkdown` |
|
| Posts/Categories/Tags admin in the proof-host SPA | D-19, SC-1 | Needs running host + admin login | Boot `sm-grzybyfunkcjonalne-app`; sign in as an administrator holding `golem15.journal.*`; open Journal nav; list/create/edit a post with `mlmarkdown` | ⬜ end-of-phase UAT |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Validation Sign-Off
|
## Validation Sign-Off
|
||||||
|
|
||||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
- [x] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||||
- [ ] Wave 0 covers all MISSING references
|
- [x] Wave 0 covers all MISSING references
|
||||||
- [ ] No watch-mode flags
|
- [x] No watch-mode flags
|
||||||
- [ ] Feedback latency < 60s
|
- [x] Feedback latency < 60s (short)
|
||||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
- [x] `nyquist_compliant: true` set in frontmatter
|
||||||
|
|
||||||
**Approval:** pending
|
**Approval:** validated 2026-10-06 (executor 15-04). Human UAT for Journal SPA remains end-of-phase. Gate `--all` is the last automated row and must print `Phase 15 gate passed`.
|
||||||
|
|||||||
395
scripts/check-phase15.sh
Executable file
395
scripts/check-phase15.sh
Executable file
@@ -0,0 +1,395 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Phase 15 fail-closed gate (Journal plugin + proof host). Every stage exits
|
||||||
|
# non-zero on a failing command, a go test run that fails, skips, matches
|
||||||
|
# zero tests, prints "no tests to run", a named required test that did not
|
||||||
|
# pass, a data race, a dirty PHP pin tree, a forbidden surface, or an
|
||||||
|
# unmitigated high threat. --self-test proves the detector fails closed on
|
||||||
|
# planted inputs. --all runs every stage and must end with
|
||||||
|
# "Phase 15 gate passed".
|
||||||
|
#
|
||||||
|
# Sibling repositories are invoked with `go -C`. Full mode runs Postgres
|
||||||
|
# integration and treats Docker unavailability as failure; -short is not
|
||||||
|
# final evidence.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
unset FORCE_COLOR
|
||||||
|
|
||||||
|
ROOT="${PHASE15_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||||
|
PLUGIN="${PHASE15_PLUGIN:-$ROOT/../sm-journal-plugin}"
|
||||||
|
HOST="${PHASE15_HOST:-$ROOT/../sm-grzybyfunkcjonalne-app}"
|
||||||
|
FONOTEKA="${PHASE15_FONOTEKA:-$ROOT/../fonoteka.go}"
|
||||||
|
PHP="${PHASE15_PHP:-/media/nvme/dev/golem15/fonoteka/plugins/golem15/journal}"
|
||||||
|
PHP_SHA="02110eb1c0c3861370b0b9b47b209a0702ac5d88"
|
||||||
|
PHASE_DIR="${PHASE15_PHASE_DIR:-$ROOT/.planning/phases/15-journal-plugin}"
|
||||||
|
REVIEW="$PHASE_DIR/15-SECURITY-REVIEW.md"
|
||||||
|
VALIDATION="$PHASE_DIR/15-VALIDATION.md"
|
||||||
|
|
||||||
|
PLUGIN_REQUIRE=(
|
||||||
|
TestJournalEndToEnd
|
||||||
|
TestJournal005DraftShow
|
||||||
|
TestJournal006MediaUpload
|
||||||
|
TestFillable
|
||||||
|
TestSearchGateOff
|
||||||
|
TestJournalWriteUnauthenticated
|
||||||
|
TestJournalPublicCategories
|
||||||
|
TestJournalPublicTags
|
||||||
|
TestJournalRSS
|
||||||
|
TestJournalFeaturedImageUnauthenticated
|
||||||
|
TestJournalCommands
|
||||||
|
TestPostsFormCompiles
|
||||||
|
TestJournalBuckets
|
||||||
|
TestJournalTables
|
||||||
|
TestJournalMigrationsRollbackAndRemigrate
|
||||||
|
TestFormatHTMLRejectsUnsafeHTML
|
||||||
|
)
|
||||||
|
HOST_REQUIRE=(
|
||||||
|
TestBootUserTranslateJournal
|
||||||
|
TestCORS
|
||||||
|
)
|
||||||
|
HIGH_THREATS=(
|
||||||
|
T-15-01 T-15-02 T-15-03 T-15-04 T-15-05 T-15-06 T-15-07
|
||||||
|
T-15-08 T-15-09 T-15-10 T-15-11 T-15-13 T-15-14 T-15-SC
|
||||||
|
)
|
||||||
|
ALL_THREATS=(
|
||||||
|
T-15-01 T-15-02 T-15-03 T-15-04 T-15-05 T-15-06 T-15-07
|
||||||
|
T-15-08 T-15-09 T-15-10 T-15-11 T-15-12 T-15-13 T-15-14
|
||||||
|
T-15-15 T-15-SC
|
||||||
|
)
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
usage:
|
||||||
|
check-phase15.sh --self-test
|
||||||
|
check-phase15.sh --php
|
||||||
|
check-phase15.sh --layout
|
||||||
|
check-phase15.sh --plugin
|
||||||
|
check-phase15.sh --host
|
||||||
|
check-phase15.sh --forbidden
|
||||||
|
check-phase15.sh --security
|
||||||
|
check-phase15.sh --all
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# detect reads go test -json. Exit 1 fail/build, 2 skip, 3 zero/no-tests,
|
||||||
|
# 4 non-JSON, 5 missing required name, 6 data race.
|
||||||
|
detect() {
|
||||||
|
python3 - "$1" <<'PY'
|
||||||
|
import json, os, sys
|
||||||
|
path = sys.argv[1]
|
||||||
|
require = [n for n in os.environ.get("REQUIRE_TESTS", "").split() if n]
|
||||||
|
passed = set()
|
||||||
|
failed = []
|
||||||
|
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 ev.get("ImportPath") or ""
|
||||||
|
if action == "build-fail" or (action == "fail" and ev.get("FailedBuild")):
|
||||||
|
print(f"refuse: build failed {pkg}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
text = ev.get("Output") or ""
|
||||||
|
if action == "output":
|
||||||
|
if "no tests to run" in text:
|
||||||
|
print(f"refuse: no tests to run in {pkg}", file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
if "WARNING: DATA RACE" in text:
|
||||||
|
print(f"refuse: data race in {pkg} {test}", file=sys.stderr)
|
||||||
|
sys.exit(6)
|
||||||
|
if action == "skip" and test:
|
||||||
|
print(f"refuse: skipped {pkg} {test}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
if action == "fail":
|
||||||
|
failed.append(f"{pkg} {test}".strip())
|
||||||
|
if action == "pass" and test:
|
||||||
|
passed.add(test)
|
||||||
|
if failed:
|
||||||
|
print("refuse: failed " + ", ".join(failed), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not passed:
|
||||||
|
print("refuse: zero tests", file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
top = {name for name in passed if "/" not in name}
|
||||||
|
missing = [n for n in require if n not in top and not any(p.startswith(n + "/") or p == n for p in passed)]
|
||||||
|
if missing:
|
||||||
|
print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr)
|
||||||
|
sys.exit(5)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
go_json() {
|
||||||
|
local dir="$1"
|
||||||
|
shift
|
||||||
|
local log err rc=0 dc=0
|
||||||
|
log="$(mktemp)"
|
||||||
|
err="$(mktemp)"
|
||||||
|
(cd "$dir" && go test -json "$@") >"$log" 2>"$err" || rc=$?
|
||||||
|
detect "$log" || dc=$?
|
||||||
|
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
|
||||||
|
cat "$err" >&2 || true
|
||||||
|
grep -v '^{' "$log" | tail -n 40 >&2 || true
|
||||||
|
rm -f "$log" "$err"
|
||||||
|
echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f "$log" "$err"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_detect() {
|
||||||
|
local name="$1" want="$2" payload="$3" log dc=0
|
||||||
|
log="$(mktemp)"
|
||||||
|
printf '%s\n' "$payload" >"$log"
|
||||||
|
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
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_self_test() {
|
||||||
|
bash -n "${BASH_SOURCE[0]}"
|
||||||
|
expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestJournalEndToEnd"}'
|
||||||
|
expect_detect fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||||||
|
{"Action":"fail","Package":"p","Test":"TestJournal005DraftShow"}'
|
||||||
|
expect_detect package-fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||||||
|
{"Action":"fail","Package":"p"}'
|
||||||
|
expect_detect build 1 '{"Action":"build-fail","ImportPath":"p"}'
|
||||||
|
expect_detect skip 2 '{"Action":"skip","Package":"p","Test":"TestSearchGateOff"}'
|
||||||
|
expect_detect zero 3 '{"Action":"pass","Package":"p"}'
|
||||||
|
expect_detect no-tests 3 '{"Action":"output","Package":"p","Output":"testing: warning: no tests to run\n"}
|
||||||
|
{"Action":"pass","Package":"p"}'
|
||||||
|
expect_detect nonjson 4 '{"Action":"pass",'
|
||||||
|
expect_detect race 6 '{"Action":"output","Package":"p","Test":"TestA","Output":"WARNING: DATA RACE\n"}
|
||||||
|
{"Action":"pass","Package":"p","Test":"TestA"}'
|
||||||
|
REQUIRE_TESTS="TestJournalEndToEnd TestFillable" expect_detect missing-named 5 \
|
||||||
|
'{"Action":"pass","Package":"p","Test":"TestJournalEndToEnd"}'
|
||||||
|
local flag
|
||||||
|
for flag in --self-test --php --layout --plugin --host --forbidden --security --all; do
|
||||||
|
grep -q -- "^ $flag)" "${BASH_SOURCE[0]}" || {
|
||||||
|
echo "refuse: missing mode $flag" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
echo "phase15 self-test passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_php() {
|
||||||
|
[[ -d "$PHP" ]] || {
|
||||||
|
echo "refuse: PHP pin tree $PHP is missing" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
local sha
|
||||||
|
sha="$(git -C "$PHP" rev-parse HEAD)"
|
||||||
|
if [[ "$sha" != "$PHP_SHA" ]]; then
|
||||||
|
echo "refuse: PHP SHA $sha, want $PHP_SHA" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -n "$(git -C "$PHP" status --porcelain)" ]]; then
|
||||||
|
git -C "$PHP" status --short >&2
|
||||||
|
echo "refuse: PHP pin tree has a diff" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "phase15 php passed ($sha)"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_layout() {
|
||||||
|
[[ -f "$PLUGIN/go.mod" ]] || {
|
||||||
|
echo "refuse: plugin go.mod missing" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
grep -q '^module git.golem15.com/golem15/sm-journal-plugin$' "$PLUGIN/go.mod" || {
|
||||||
|
echo "refuse: plugin module path" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
grep -q '^replace git.golem15.com/golem15/summercms => ../summercms.go$' "$PLUGIN/go.mod" || {
|
||||||
|
echo "refuse: plugin must replace summercms => ../summercms.go" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if grep -E '^replace .+sm-user-plugin|^replace .+sm-translate-plugin' "$PLUGIN/go.mod" >/dev/null; then
|
||||||
|
echo "refuse: plugin go.mod must not replace sibling plugins" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
[[ -f "$HOST/go.work" && -f "$HOST/plugins.gen.go" && -f "$HOST/summer.yaml" ]] || {
|
||||||
|
echo "refuse: host layout is incomplete" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
local line
|
||||||
|
for path in plugins/golem15/user plugins/golem15/translate plugins/golem15/journal; do
|
||||||
|
line="$(git -C "$HOST" ls-files -s "$path")"
|
||||||
|
[[ "$line" == 160000* ]] || {
|
||||||
|
echo "refuse: $path is not a gitlink: $line" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
grep -q 'golem15.user' "$HOST/plugins.gen.go" || {
|
||||||
|
echo "refuse: plugins.gen.go missing golem15.user" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
grep -q 'golem15.translate' "$HOST/plugins.gen.go" || {
|
||||||
|
echo "refuse: plugins.gen.go missing golem15.translate" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
grep -q 'golem15.journal' "$HOST/plugins.gen.go" || {
|
||||||
|
echo "refuse: plugins.gen.go missing golem15.journal" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if grep -E 'acme\.fixture' "$HOST/plugins.gen.go" >/dev/null; then
|
||||||
|
echo "refuse: production plugin list contains fixture" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! grep -q '_journal/api/\*' "$HOST/config/http.yaml"; then
|
||||||
|
echo "refuse: host CORS missing _journal/api/*" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "phase15 layout passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_plugin() {
|
||||||
|
[[ -d "$PLUGIN" ]] || {
|
||||||
|
echo "refuse: plugin repository $PLUGIN not found" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
go -C "$PLUGIN" vet ./...
|
||||||
|
REQUIRE_TESTS="${PLUGIN_REQUIRE[*]}" go_json "$PLUGIN" ./... -count=1 -timeout 20m
|
||||||
|
REQUIRE_TESTS="${PLUGIN_REQUIRE[*]}" go_json "$PLUGIN" ./... -count=1 -race -timeout 25m
|
||||||
|
echo "phase15 plugin passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_host() {
|
||||||
|
[[ -d "$HOST" ]] || {
|
||||||
|
echo "refuse: proof host $HOST not found" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
go -C "$HOST" vet ./...
|
||||||
|
REQUIRE_TESTS="${HOST_REQUIRE[*]}" go_json "$HOST" ./... -count=1 -timeout 5m
|
||||||
|
go -C "$HOST" build -o /tmp/phase15-host ./...
|
||||||
|
rm -f /tmp/phase15-host
|
||||||
|
echo "phase15 host passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_forbidden() {
|
||||||
|
local bad=0 hits
|
||||||
|
hits="$(cd "$PLUGIN" && grep -RInE 'rainlab_journal_|winter_journal_' --include='*.go' . | grep -vE '_test\.go:' || true)"
|
||||||
|
if [[ -n "$hits" ]]; then
|
||||||
|
echo "refuse: Winter/RainLab journal table names in plugin Go: $hits" >&2
|
||||||
|
bad=1
|
||||||
|
fi
|
||||||
|
hits="$(cd "$PLUGIN" && grep -RInE 'plugin\.Open|yaegi' --include='*.go' . | grep -vE '_test\.go:' || true)"
|
||||||
|
if [[ -n "$hits" ]]; then
|
||||||
|
echo "refuse: runtime loading in production plugin: $hits" >&2
|
||||||
|
bad=1
|
||||||
|
fi
|
||||||
|
hits="$(cd "$PLUGIN" && grep -RInE '\.AutoMigrate\(' --include='*.go' . | grep -vE '_test\.go:' || true)"
|
||||||
|
if [[ -n "$hits" ]]; then
|
||||||
|
echo "refuse: AutoMigrate in production plugin: $hits" >&2
|
||||||
|
bad=1
|
||||||
|
fi
|
||||||
|
hits="$(cd "$PLUGIN" && grep -RInE 'sm-user-plugin' --include='*.go' . | grep -vE '_test\.go:' || true)"
|
||||||
|
if [[ -n "$hits" ]]; then
|
||||||
|
echo "refuse: production plugin imports sm-user-plugin: $hits" >&2
|
||||||
|
bad=1
|
||||||
|
fi
|
||||||
|
hits="$(cd "$PLUGIN" && grep -RInE 'fonoteka|p[lł]ytarium|grzybyfunkcjonalne' README.md || true)"
|
||||||
|
if [[ -n "$hits" ]]; then
|
||||||
|
echo "refuse: consuming-application name in plugin README: $hits" >&2
|
||||||
|
bad=1
|
||||||
|
fi
|
||||||
|
hits="$(cd "$PLUGIN" && grep -RInE 'Pages menu|dashboard widget|journalPost|journalPosts' --include='*.go' . | grep -vE '_test\.go:' || true)"
|
||||||
|
if [[ -n "$hits" ]]; then
|
||||||
|
echo "refuse: deferred Pages/dashboard/theme surface in production plugin: $hits" >&2
|
||||||
|
bad=1
|
||||||
|
fi
|
||||||
|
if [[ -n "$(git -C "$ROOT" diff -- modules/cabana/field_markdown.go)" ]]; then
|
||||||
|
echo "refuse: modules/cabana/field_markdown.go changed this phase (D-11 no-op)" >&2
|
||||||
|
bad=1
|
||||||
|
fi
|
||||||
|
local tide
|
||||||
|
tide="$(grep -RIn '/_journal/api/v1' "$FONOTEKA/parity" "$FONOTEKA/modules/tide" "$ROOT/modules/tide" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$tide" ]]; then
|
||||||
|
echo "refuse: tide/parity harness newly mentions /_journal/api/v1: $tide" >&2
|
||||||
|
bad=1
|
||||||
|
fi
|
||||||
|
hits="$(gofmt -l "$PLUGIN" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$hits" ]]; then
|
||||||
|
echo "refuse: gofmt: $hits" >&2
|
||||||
|
bad=1
|
||||||
|
fi
|
||||||
|
[[ "$bad" -eq 0 ]] || return 1
|
||||||
|
echo "phase15 forbidden passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_security() {
|
||||||
|
[[ -f "$REVIEW" ]] || {
|
||||||
|
echo "refuse: missing $REVIEW" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
[[ -f "$VALIDATION" ]] || {
|
||||||
|
echo "refuse: missing $VALIDATION" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
local id count
|
||||||
|
for id in "${ALL_THREATS[@]}"; do
|
||||||
|
count="$(grep -c -- "$id" "$REVIEW" || true)"
|
||||||
|
if [[ "$count" -lt 1 ]]; then
|
||||||
|
echo "refuse: security review missing $id" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if grep -qiE 'unmitigated high' "$REVIEW"; then
|
||||||
|
echo "refuse: security review still has an unmitigated high finding" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
for id in "${HIGH_THREATS[@]}"; do
|
||||||
|
grep -q -- "$id" "$REVIEW" || {
|
||||||
|
echo "refuse: high threat $id missing" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
grep -A2 -- "$id" "$REVIEW" | grep -qi mitigate || {
|
||||||
|
echo "refuse: high threat $id is not marked mitigate" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
if ! grep -q 'No external API integration' "$REVIEW" && ! grep -qi 'no external SaaS SDK' "$REVIEW"; then
|
||||||
|
echo "refuse: security review must state there is no external SaaS SDK" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! grep -q 'nyquist_compliant: true' "$VALIDATION"; then
|
||||||
|
echo "refuse: VALIDATION is not signed off" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "phase15 security passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_all() {
|
||||||
|
local stage
|
||||||
|
for stage in self-test php layout plugin host forbidden security; do
|
||||||
|
if bash "${BASH_SOURCE[0]}" "--$stage"; then
|
||||||
|
echo "PASS $stage"
|
||||||
|
else
|
||||||
|
echo "FAIL $stage"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Phase 15 gate passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:---all}" in
|
||||||
|
--self-test) run_self_test ;;
|
||||||
|
--php) run_php ;;
|
||||||
|
--layout) run_layout ;;
|
||||||
|
--plugin) run_plugin ;;
|
||||||
|
--host) run_host ;;
|
||||||
|
--forbidden) run_forbidden ;;
|
||||||
|
--security) run_security ;;
|
||||||
|
--all) run_all ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user