Repository navigation
feat(ssp-templates): discovery planner and preview route - #524
Merged
gusfcarvalho merged 3 commits intoOct 22, 2026
Merged
Conversation
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Add the pure planner (ssptemplates.BuildPlan) shared by preview and the discovery job, and POST /api/admin/ssp-templates/preview, which plans a saved or unsaved template against the latest evidence and writes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ccf-lisa
Bot
force-pushed
the
feat/ssp-templates-3-planner
branch
from
October 19, 2026 14:22
48e5317 to
4b18bcb
Compare
This was referenced Oct 19, 2026
gusfcarvalho
requested changes
Oct 19, 2026
LoadLatestEvidenceStreams folds label values for matching, so plans lower-cased SSP titles, key values and learned scope entries. The loader now also returns the trimmed original values (RawLabels); the planner still filters, checks keys and groups on the folded labels, but renders and learns from the first stream's (by UUID) original casing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho
pushed a commit
that referenced
this pull request
Oct 22, 2026
## Place in the stack SSP templates (feature 2 of `ssp-templates.md`), PR **B1 of 5** (B1–B5: #522, #523, #524, #525, #526), stacked on the SSP scope stack (#516–#520, top `feat/ssp-scope-5-auto-filters`). ## What it adds - Models `SSPTemplate` and `SSPTemplateInstance` (`internal/service/relational/ssp_templates.go`), in both AutoMigrate lists and both drop lists. Their foreign keys (instance → template `ON DELETE CASCADE`, instance → SSP `ON DELETE SET NULL`) are added by `EnsureSSPConstraints`, which is `EnsureSSPScopeConstraints` renamed, with two keys appended. AutoMigrate here does not enforce gorm FK tags. - Package `internal/service/relational/ssptemplates`: - `Build`: validation and normalization. Keys are trimmed, lower-cased and sorted (1–5). Scope-label groups are normalized the same way (≤10 groups of 1–5 labels). Keys and labels follow `sspscope.Normalize`'s key rules, so anything a template accepts can be stored as a scope entry. `min-streams` must be ≥ 1. The title template is required, and the title, description and prop templates must parse. The labelfilter must be valid. - `Service`: list/get/create/update/delete. Profile ids must exist. Keys cannot change on update (400). - Admin routes `/api/admin/ssp-templates` (list, create, get, update, delete), behind `JWTMiddleware` + `pep.Authorize(authz.ResourceAdmin, authz.ActionManage)`, like `/admin/subject-templates`. JSON is kebab-case. Swagger is regenerated. - **Contract addition from review**: template responses also return `ssp-count` and `last-run-at`. - `ssp-count` is the number of instances whose SSP still exists. - `last-run-at` is a new nullable `last_run_at` column. It stays `null` until B4's discovery job sets it, and Update preserves it. Not in this PR: preview (B3), SSP creation (B2/B4), discovery, run, instances routes (B4). ## How it was tested - Unit: `ssptemplates/validate_test.go`. It covers normalization, defaults, and every rejection rule. - Integration (`internal/api/handler/templates/ssp_template_integration_test.go`) runs under the embedded Cedar PDP: - an admin creates, lists, gets, updates and deletes templates; - changing keys is rejected; - a contributor gets 403 on every route; - 400/404 cases; - the FK behaviour and `ssp-count`: deleting an SSP nulls its instance's `ssp_id` and drops it from `ssp-count`, and deleting a template drops its instances and keeps its SSPs. - Gate on this branch: - `make swag`, `make lint` (only the existing SA1019 in `internal/api/metrics.go`), `make test`, `go vet -tags integration ./...`; - integration suites of `internal/api/handler`, `internal/api/handler/templates`, `internal/service`, `internal/service/relational`, `internal/service/relational/sspscope`; - `make check-diff`. Changed lines excluding generated swagger: 1000. ## Deviations from design - The instance model has exactly the design's fields. The `skipped-entries` count and sample reported by the instances route are added in B4, with the code that writes them. - Duplicate scope-label groups (after normalization) are rejected with 400, not merged silently. Duplicate profile ids are de-duplicated. - An empty filter (`{}` / `{"scope":{}}`) matches everything, so it is stored as no filter. - The `enabled` column has no database default (the design has `default:true`). gorm writes a column default over a false `Enabled` on create, so a disabled template was stored as enabled. `Build` always sets `Enabled`, with nil meaning true. - The list route is paginated (`svc.ListResponse`, `page`/`limit`) like subject templates. - The response timestamps are kebab-case (`created-at`/`updated-at`), unlike subject templates' camelCase ones. ## Open questions for reviewers - Templates are only checked to parse. Should save also reject templates that reference a field that is not one of the keys (keys are lower-cased, so `{{ ._APP }}` would render empty)? The design asks only for parse errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho
pushed a commit
that referenced
this pull request
Oct 22, 2026
## Place in the stack SSP templates (feature 2 of `ssp-templates.md`), PR **B4 of 5**. Base: B3 (#524, `feat/ssp-templates-3-planner`). Below it: B2 (#523), B1 (#522) and the SSP scope stack (#516–#520). The design's B4 is split in two to stay under 1000 lines. This PR is discovery. B5 (`feat/ssp-templates-5-routes`) adds the run route, the instances list (stale, `key-hash`, `skipped-entries`) and the forget route. ## What it adds - `ssptemplates.Discovery` runs the B3 planner against the latest evidence and applies it: - **New group:** creates the SSP through B2's `sspservice` in a transaction holding the template's advisory lock, records the instance, and learns the scope (`sspscope.Service.Learn`, origin `template`). - The SSP id is `uuid.NewSHA1(namespace, templateID || keyHash)`. - `metadata.title` is the rendered title and `metadata.version` is `1.0`. - Props are the rendered props plus `ssp-template=<template id>`. - `system-name` is the title, the description is rendered (or falls back to the title), and status is `under-development`. Profiles are attached after that transaction commits. A failed attach is recorded in `last_error` and `profiles_pending`, and the next run retries it. - **Existing group:** refreshes `last_seen_at` and learns new scope entries. Learn already skips exceptions and existing entries. - **Deleted SSP** (instance `ssp_id` null): left alone, never recreated. - At most `MaxNewSSPsPerRun` (100) new SSPs per template per run; the next run creates the rest. - Learned entries the scope cannot hold (from the planner, and `LearnResult.Skipped`) go into new instance columns `skipped_entries` and `skipped_sample` (up to 5). - A failing group gets `last_error` and the run's error; the other groups still apply. `last_run_at` is set when the run finishes. This is the contract addition from review: B1 exposes it as `last-run-at`, null until this job sets it. - River worker and periodic job `ssp_template_discovery` on the `scheduler` queue. Empty args run every enabled template, loading the evidence once; args with `template_id` run one template, for B5's run route. - The worker reuses the `ProfileControlResolver` that `cmd/run.go` already injects (the oscal handler's control resolution), adapted to `sspservice.ControlResolver`. - Config: `CCF_SSP_TEMPLATE_DISCOVERY_SCHEDULE` (default hourly, `0 37 * * * *`) and `CCF_SSP_TEMPLATE_STALE_AFTER` (default `168h`, used by B5), in `internal/config` and `.env.example`. ## How it was tested - Integration (`ssptemplates/discovery_integration_test.go`, testcontainers): - the SSP is created with the evidence's casing, the deterministic id, title, description, props, the template's profile and its implemented requirements; scope entries have origin `template` and the template id; `last_run_at` is set; - runs are idempotent (one SSP, `last_seen_at` refreshed); - later runs learn new values (evidence in other casing joins the same SSP) and respect exceptions; - a deleted SSP is never recreated; - the 100-per-run cap: 101 groups give 100, then 1; - a failed profile attach is recorded and then retried successfully; - disabled templates are skipped; - invalid learned entries are counted and sampled. - Unit: the config loader, the schedule fallback, and the resolver adapter's case-insensitive de-duplication. - Gate on this branch: - `make swag` (no doc change), `make lint` (only the existing SA1019), `make test`, `go vet -tags integration ./...`; - integration suites of `internal/service/worker`, `ssptemplates`, `internal/service`, `internal/service/relational`, `internal/config`; - `make check-diff`. Changed lines excluding generated swagger: 762. ## Deviations from design - The design's B4 is split into B4 (discovery) and B5 (routes). - New instance columns, not in the design's model: - `profiles_pending`: retry an attach only while one failed, so a profile a user removed is never re-attached; - `skipped_entries` / `skipped_sample`: the design's "skipped count and a sample". - The `ssp-template` prop uses the existing `relational.CCFOSCALNamespace` (`https://compliance-framework.github.io/ns/oscal`), not `…/ns`. - Rendered prop values that come out empty are dropped, because OSCAL props need a value. - `plan.InvalidEntry` is now an alias of `relational.SkippedScopeEntry`. ## Open questions for reviewers - An existing SSP whose evidence drops under `min-streams` is not refreshed, so it can be reported stale even though some evidence remains. Should `min-streams` apply only to creating SSPs? - The `ssp-template` prop namespace: the existing `…/ns/oscal` constant, or the design's `…/ns`? 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho
pushed a commit
that referenced
this pull request
Oct 22, 2026
## Place in the stack SSP templates (feature 2 of `ssp-templates.md`), PR **B5 of 5**. It is the second half of the design's B4, split to keep each PR under 1000 lines. Base: B4 (#525, `feat/ssp-templates-4-discovery`). Below it: B3 (#524), B2 (#523), B1 (#522) and the SSP scope stack (#516–#520). ## What it adds All three routes sit behind the existing `/api/admin/ssp-templates` group: JWT + `admin:manage`. - `POST /api/admin/ssp-templates/{id}/run` queues a discovery run of the template (`ssp_template_discovery` with the template id as args) and answers **202**. - 404 for an unknown template, **409** for a disabled one. - 503 when no job queue is wired, and 500 when the worker is disabled. - Runs are unique by args among waiting or running jobs, so a finished run never blocks the next one. - `worker.Service.EnqueueSSPTemplateDiscovery` implements the new `templates.SSPTemplateRunEnqueuer`, wired through `APIServices.SSPTemplateRunEnqueuer` in `cmd/run.go`. - `GET /api/admin/ssp-templates/{id}/ssps` is paginated and newest first. Each item has: - `key-values` and **`key-hash`** (a contract addition from review: the forget route's id); - `ssp-id` (null once the SSP was deleted) and the SSP's current `title`; - `created-at` and `last-seen-at`; - `stale`: the SSP exists and was last seen more than `CCF_SSP_TEMPLATE_STALE_AFTER` ago (from B4's config); - `last-error`, `skipped-entries` and `skipped-sample`. - `DELETE /api/admin/ssp-templates/{id}/ssps/{keyHash}` forgets an instance whose SSP was deleted, so the next run creates it again. It answers 409 while the SSP exists and 404 for an unknown instance. Swagger is regenerated. ## How it was tested - Integration under the embedded Cedar PDP (`ssp_template_instances_integration_test.go`): - run: queued once, disabled 409, unknown 404, contributor 403, no job queue 503; - list: key values, key hash, title, stale/fresh/deleted, last error, skipped entries; contributor 403; unknown template 404; - forget: 409 while the SSP exists, 404 unknown, contributor 403, 204 for a deleted SSP, and the list shrinks afterwards. - Unit: enqueueing with the worker disabled is an error. - Gate on this branch: - `make swag`, `make lint` (only the existing SA1019), `make test`, `go vet -tags integration ./...`; - integration suites of `internal/api/handler`, `internal/api/handler/templates`, `ssptemplates`, `internal/service/worker`; - `make check-diff`. Changed lines excluding generated swagger: 395. ## Deviations from design - This PR exists only because B4 was split. - The instance list is paginated, and adds `title`, `key-hash` (contract addition) and `skipped-sample` to the design's fields. - A run requested while the worker is disabled answers 500 instead of a 202 for a run that would never happen. ## Open questions for reviewers - Should a run requested while the worker is disabled answer 503 rather than 500? Telling that case apart from an enqueue failure needs an error exported from the worker package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Place in the stack
SSP templates (feature 2 of
ssp-templates.md), PR B3 of 5. Base: B2 (#523,feat/ssp-templates-2-ssp-service). Below it: B1 (#522) and the SSP scope stack (#516–#520).What it adds
ssptemplates.BuildPlan(tmpl, streams, instances, entries, exceptions)is the pure planner that preview and B4's discovery job share. It:missing-keys;min-streamsasbelow-threshold;sspscope.Normalize, or whose title fails to render or renders empty, underinvalid;min-streamsof the group's streams. Multi-valued labels expand to every combination.skipped-by-exception, and moves entries a scope cannot hold toinvalid-entries;LatestEvidenceStreamgainsRawLabels: the trimmed original values, index for index with the foldedLabels, which are unchanged for other callers. The filter, key checks and grouping still use the folded labels, soPaymentsandpaymentsare one group with oneKeyHash. Titles, key values and learned entries come from the group's first stream by UUID; scope-label combinations count case-insensitively.existswhen an instance records it. An instance whose SSP was deleted (ssp-idnull) gets nothing to learn.POST /api/admin/ssp-templates/previewtakes a template, saved or not, with an optionalidso a saved template's instances mark existing SSPs. It loads the latest evidence and the instances' scope hashes, runs the planner and writes nothing. It sits behind the group's JWT +admin:manage. Swagger is regenerated.How it was tested
plan_test.go):TestPreview):make swag,make lint(only the existing SA1019),make test,go vet -tags integration ./...;internal/api/handler/templates,ssptemplates,sspscope,internal/service/workerandinternal/service/relational/evidence;make check-diff.Changed lines excluding generated swagger: 662.
Deviations from design
BuildPlan, becausePlanis its result type. Its arguments are(tmpl, streams, instances, entries, exceptions).key-hash,ssp-idandinvalid-entriesto each planned SSP.missing-keysalso counts streams whose key is multi-valued, since they cannot be grouped.below-thresholdlists groups only. Scope-label combinations undermin-streamsare dropped without being listed.invalid, so no SSP with an empty title is created.Open questions for reviewers
below-thresholdalso list scope-label combinations undermin-streams? The design's wording says "groups or entries", but its example shows groups only.🤖 Generated with Claude Code