Releases: risk-sentinel/sparc
Release list
v1.15.3 — control identifier consistency, fail-closed DB password, POA&M generator
SPARC v1.15.3 is an integrity and consistency release. The through-line: the same control could be identified two different ways depending on which document you exported, and nothing would ever tell you. Alongside that, an operator could run SPARC against an unauthenticated database by omission, PIV sign-in was broken behind an AWS ALB, and the POA&M — the artifact customers touch monthly — was the one document in the authorization chain that could not be generated in-app.
Documents now agree with each other
One canonical control identifier (#852). SPARC had at least a dozen ad-hoc control-id transformers: four byte-identical copies of one normalizer, three padding implementations, two de-padding ones, and a weaker variant in the mapping export. None reconciled zero-padding, so AC-02 and ac-2 were different strings despite naming one control. That is what broke documents flowing Catalog → ATO Package.
The catalog was always right — its importer stores canonical as the identifier and padding only as a sort key. The defect was downstream failing to resolve against it.
Two consequences, now closed:
- A mapping wrote
ac-2-(1)while the SSP, SAP, SAR and POA&M wroteac-2.1for the same control. No validator would ever flag this —id-refisStringDatatypein the mapping schema — so nothing linked a mapping entry to a control in another document, silently. - A control selection of
ac-2matched nothing against a storedAC-02, producing an empty SAP reported as success.
Also fixed: the POA&M export passed target_data through verbatim, so a finding carrying AC-2 (1) made the entire document fail validation at export; and the profile export wrote with-ids and modify/alters/control-id raw. Both are TokenDatatype.
The evidence is the feature. Every export that publishes a control identifier — ssp, assessment-results, assessment-plan, component-definition, mapping, poam, profile — is now asserted to emit legal OSCAL tokens and validate, under deliberately hostile inputs (AC-2 (1), AC-02, AC-2(1), whitespace-padded). Schema validity alone was never sufficient evidence, which is exactly how ac-2-(1) shipped unnoticed.
Normalisation covers form, not vocabulary: CCI-000213 is normalised in shape and never translated into a NIST control, because mapping an external identifier is a lookup, not a string transform.
Fail closed rather than run insecure
A missing database password no longer connects anyway (#849). A SPARC_DB_* block without SPARC_DB_PASSWORD used to connect with no password at all — nil reaches libpq, a server on trust auth accepts it, and the app boots, passes health checks and serves traffic against an unauthenticated database. Nothing looked wrong.
Production now refuses to start, naming the unset variable and every supported fix. Development and test are unaffected. SPARC_DB_ALLOW_EMPTY_PASSWORD=true is the explicit opt-out for RDS IAM authentication — running without a database password should be a decision, not something that happened to you.
Also fixed on that path: "" is truthy in Ruby, so an explicitly-empty SPARC_DB_PASSWORD beat the fallbacks and was handed to libpq as a real empty password.
PIV sign-in behind an ALB
Smart-card login failed on a complete, gateway-verified certificate (#850). safe_unescape used CGI.unescape, which implements form encoding and decodes + as a space. Base64 uses + as data, and ALB passthrough leaves it literal — so every + became a space and the whitespace strip then deleted it, losing one character per +.
The corruption passed every internal check: the shortened body still matched the base64 charset guard and the minimum length, so the certificate reported as normalized and only OpenSSL rejected it. The v1.15.1 test could not catch it because it encoded with CGI.escape, which writes + as %2B — it round-tripped its own encoder.
The lifecycle can be completed in-app
POA&M generator (#843). Every other document in the chain had one; POA&M had only parsers and an exporter, so the terminal artifact of an ATO could enter SPARC only by importing external OSCAL or hand-assembling its object graph. A SAR's open risks now become POA&M items, carrying findings and observations across through the assessor's own linkage.
Nothing is synthesised. A source record that cannot form a valid POA&M entry is skipped and reported with a reason, never completed on the author's behalf — a risk statement, a deadline and a finding target are compliance content an AO reads. The one derived value is deadline, and only from the admin-provisioned RemediationTimeline SLA; if no window resolves, the risk is skipped rather than given an arbitrary date.
Standalone SAP generation (#844). POST /api/v1/sap_documents/generate builds a populated SAP from an SSP, a profile, or just a boundary — a plan is not a once-per-authorization artifact, and a 3PAO or automated pipeline could previously create only an empty shell.
Storage, security and hygiene
Structured object keys (#830). Every blob landed in a flat namespace with a random key, so prefix-scoped IAM was impossible and evidence under a retention obligation could not be separated from a user avatar. Keys are now sparc/… for instance-wide and <organization>/<boundary>/… for tenant data, so the bucket root is the tenant list. Migration is copy-then-update via sparc:storage:audit and sparc:storage:relocate (dry-run unless APPLY=true).
Cross-boundary evidence disclosure (#851). KSI validations accepted an evidence_id from another authorization boundary, and the detailed serializer returned that evidence's title, type, status and file hash. Found while triaging CodeQL — which had flagged the safe thing on that line and missed this two arguments away.
CodeQL triage (#846). All 8 open alerts confirmed false positives and dismissed with their specific claims recorded.
yamllint (#847). 815 findings, all line-length, none structural. The limit now applies to hand-maintained config with the rest exempt from that rule only.
Behaviour changes worth reading before upgrading
- Production refuses to start without a database password. Set
SPARC_DB_PASSWORD, useDB_CREDENTIALS, or declareSPARC_DB_ALLOW_EMPTY_PASSWORD=true. - Exports emit unpadded control ids. A document with padded stored controls previously exported
ac-02and now exportsac-2. Both are legal OSCAL tokens; unpadded is what every other export already produced and is what makes cross-document linkage work. - New blobs use structured keys. Existing blobs keep theirs — a mixed layout is normal and both remain readable.
SSP_TPR_MANAGER_DATABASE_PASSWORDis now documented as deprecated, scheduled for removal in 1.16.0.
Verification
Full suite 3,730 examples / 0 failures. API contract suite 385 / 0. Playwright ui-smoke 320 / 0 with two previously-uncovered accessibility baselines captured. The latter two ran against the UBI9 production image over TLS, where structured storage keys, canonical control ids and CCI preservation were each confirmed in a running production environment.
Closes: #830, #843, #844, #846, #847, #849, #850, #851, #852
v1.15.2 — OSCAL export conformance, POA&M integrity, credential recovery
SPARC v1.15.2 is a correctness and integrity release. The through-line: SPARC could produce documents that were not valid OSCAL, and only find out much later — or never. Every fix here moves the failure to the point of entry, and none of them fills a gap by inventing content.
It also closes a credential-recovery hole and makes RDS password rotation work without a redeploy.
OSCAL export conformance
Every OSCAL XML export was schema-invalid (#827). The converter walked the parsed JSON in insertion order, but each OSCAL assembly is an xs:sequence with a mandated child order. download_xml is a user-facing route on seven document types and all seven failed their XSD.
Ordering turned out to be the first of five defects, all the same mistake — treating a per-type schema fact as a global rule:
| Before | Now | |
|---|---|---|
| element order | JSON key order | the parent's xs:sequence |
| attributes | one global list | per type — name is an attribute of <prop> but an element of <party> |
| prose | chosen by key name | markup-line (text) vs markup-multiline (<p>), from the schema |
| text content | n/a | xs:simpleContent puts the value in the text position |
| array names | a hand-written plural map | schema-resolved, incl. wrappers (revisions) and renames (remediations → <response>) |
The ordering table is generated from the same XSDs the validator uses, so the order used to write XML cannot disagree with the order used to check it. A drift spec regenerates and compares, so it cannot rot.
The ATO package (#828, #829). The manifest was built from the boundary's associations while the archive was built from exports that could fail — so a failed export was skipped silently while the manifest went on naming the missing file. Exports now run first and both the archive and the manifest derive from the same results, so they cannot disagree. Omissions are stated in the manifest with their error.
The package also shipped JSON only; it now emits JSON, YAML and XML, each validated against the schema matching its serialization, with format and conformance recorded per file.
sar_from_hdf returned schema-invalid OSCAL unchecked (#831). A consumer got a 200 and a document no OSCAL tool would accept. It now validates before returning.
POA&M integrity
PoamRisk validated only uuid (#832). A risk could be saved with no title, description, statement, status or deadline — invalid OSCAL could be created, failing only at export with nothing to say which record was at fault. Now validated on every save. deadline is a SPARC rule rather than an OSCAL one: a POA&M with no time commitment is not a plan, and hdf convert refuses it.
HDF aggregation made the POA&M non-exportable (#840). HdfAggregationService wrote findings with no OSCAL target, so one aggregation run made the entire document fail schema validation in every serialization — the user met it at export as ?oscal_validation_failed=1. The target is now derived from the control the scanner actually assessed.
Nothing in either fix is defaulted or synthesised: a risk statement, a deadline and a finding target are all substantive compliance content. Pre-existing rows are found with bin/rails sparc:poam:audit_risks and sparc:poam:audit_findings, which deliberately offer no auto-fix.
Credentials and recovery
A forgotten local-login password was unrecoverable (#841). No admin reset, no self-service flow, and the password-change screen requires the current password — which is exactly what has been lost. The only way back in was a Rails console. Two routes now exist, and neither leaves an administrator holding a password the user keeps:
- Temporary password — works anywhere including with no outbound mail. The admin hands it over out of band and the user must replace it at first sign-in.
- Emailed link — a single-use, expiring token (requires SMTP).
The break-glass admin credential could not stay in sync (#841). Bootstrap forced a password change even when the password came from Secrets Manager, which made a loop: the operator complies, the credential recorded in SM is now stale, and the next restart overwrites their choice and forces another change. A forced change now applies only when SPARC generated the password itself.
RDS connection from DB_CREDENTIALS (#834). DATABASE_URL is rendered at deploy time and therefore pins the password, so a Secrets Manager rotation had no effect until the next redeploy. SPARC now prefers the structured secret, read at boot — a rotated password is picked up on the next task restart with no IaC change. This unblocks the DB half of sparc-iac#531.
Three defects were found and fixed on that path during review: the password was rendered into unquoted YAML (a value containing & was read as a YAML anchor and became nil — connecting with no password); the container entrypoint resolved the database host in the shell using a different precedence than Rails, so it could wait on one database and connect to another; and database credentials could reach CloudWatch through driver errors and inspect output. Credentials are now redacted from logs by default, with SPARC_LOG_CREDENTIALS=true as a deliberate, loudly-warned escape hatch.
Behaviour changes worth reading before upgrading
POST /api/v1/oscal/sar_from_hdfreturns 502 for real HDF input instead of 200-with-invalid-OSCAL, until a fixed hdf-libs is pinned (mitre/hdf-libs#184). The caller's input is fine; the fault is upstream, which is what a Bad Gateway means.- POA&M imports are stricter. Third-party POA&Ms with incomplete risks or findings are now rejected rather than silently creating invalid records.
- Pre-existing incomplete
PoamRisk/PoamFindingrows are unsaveable until completed. That is the point; the audit tasks exist to find them. - The ATO package contains 3× the files — one per serialization.
DB_CREDENTIALSoutranksDATABASE_URL, and removes it from the environment when both are set. The resolved source is now reported at boot, with a warning when a second source is configured but ignored.
Also included
- hdf-cli pinned to 3.4.1 across the repo (#837) pending mitre/hdf-libs#184.
- Dependency updates: patch group (aws-sdk-s3, csv, net-imap, pagy, simplecov) and solid_queue 1.5.0.
openssl4.x was declined — it would revert a deliberate pin (#779), andbundle-auditreports no vulnerabilities on the current lock.
Verification
Full suite 3,537 examples / 0 failures; API contract suite 385 / 0; Playwright 201 / 0 — the latter two run against the UBI9 production image over TLS, not the Debian dev compose.
v1.15.1 — PIV/CAC sign-in fix (supersedes unsigned v1.15.0)
Patch release fixing PIV/CAC sign-in, which was impossible in v1.15.0, and re-establishing a signed release image.
Upgrade note. v1.15.0's image published successfully but its signing job never ran — the build job failed on a GitHub Actions cache-export error after the image had already been pushed, which skipped
sign-verify-attest. v1.15.0 is therefore unsigned and is superseded by this release. Deployments verifying signatures should move to v1.15.1.
Fixed
PIV/CAC sign-in failed for every user (#824)
A user selected their smart-card certificate, entered the card PIN, and was told "No smart card certificate was presented" — while the gateway was forwarding the certificate and attesting verify=SUCCESS. Firefox, Chrome and Edge behaved identically, because the fault was server-side.
Three defects, two of which independently broke sign-in.
The certificate arrived but could not be parsed. HTTP headers cannot carry newlines and a PEM is defined by them, so every gateway works around it differently:
| Gateway | What arrives |
|---|---|
nginx $ssl_client_escaped_cert, ALB X-Amzn-Mtls-Clientcert |
URL-encoded (%0A) |
nginx $ssl_client_cert |
newlines folded to spaces or tabs |
| assorted | literal backslash-n written into the value |
| assorted | bare base64 DER with no BEGIN/END markers |
SPARC handled exactly one of these — it URL-decoded when it saw a percent sign. Every other shape reached OpenSSL as mangled text and was rejected. SPARC now reassembles all four before parsing, so no additional gateway configuration is required. Reassembly remains fail-closed: anything that is not a decodable certificate is still refused.
The failure message was misleading. A missing certificate and an unreadable one produced the same message, so the audit log reported "no certificate presented" while the gateway simultaneously reported successful verification — a contradiction that points diagnosis at the wrong subsystem. The two are now distinct:
| Message | Meaning |
|---|---|
| Your smart card could not be verified by the gateway. | Verify header missing or not SUCCESS |
| No smart card certificate was presented. | Verify succeeded but the certificate header was empty |
| Your smart card certificate could not be read. | A certificate arrived but could not be decoded |
Every PIV failure now records shape-only diagnostics — byte count, whether PEM markers were present, whether the value was URL-encoded, whether reassembly succeeded — so the next occurrence is diagnosable from the audit log without a redeploy. The certificate itself is never recorded, in whole or in part.
The consent banner did not persist. It re-fired on every login-page load, interposing between the certificate-bearing request and /auth/piv. Acceptance is now remembered for the browser session. Session scope is deliberate: AC-8 requires the notice before access is granted, so acceptance is not made permanent — clearing the browser session brings the notice back.
Documentation
- Authentication and MFA (wiki) — the forwarded-certificate formats SPARC accepts, and a troubleshooting table mapping each smart-card failure message to where to look.
Compliance
- Authentication component definition 1.1.0 → 1.2.0, and the NIST SP 800-53 Rev 5 mapping, updated for IA-2 / IA-2(12) to cover forwarded-certificate reassembly and the distinct auditing of absent versus undecodable certificates.
Verification
- Full RSpec suite: 3376 examples, 0 failures, 24 pending (environment-gated).
- Seven parser tests, one per gateway mangling, plus request-level tests signing in through the real request path with space-folded, URL-encoded and marker-less certificates.
- Mutation-tested: with certificate reassembly disabled, all seven mangling tests fail while the well-formed control and every negative case still pass.
- Browser tests for consent persistence pass against this build and fail against v1.15.0.
Full changelog: v1.15.0...v1.15.1
v1.15.0 — HDF Aggregation: re-occurrence lifecycle, amendment approval, document aggregation
Extends HDF triage from a per-scan worklist into an audit-ready, ATO-integrated flow. Scans are tied to the component they assessed, kept as history with a re-occurrence lifecycle, gated by an approval + ODP-validity flow, and folded into the boundary's assessment documents.
Includes v1.14.0. The HDF Amendment Triage work (#447) set
VERSION 1.14.0onmainbut was never tagged, so nov1.14.0release or image exists. Everything from that release ships here. Upgrading from v1.13.2 → v1.15.0 is the supported path.
HDF Aggregation (#809, #811)
- Audit-ready scan history + re-occurrence lifecycle (#811) — each ingest records its Target / CDEF and Scope (target-specific vs boundary-wide), and findings are kept per-scan-run as history (
currentflag) rather than overwritten. Every finding carries a lifecycle status —new/carried_forward/re_failed(failing again at worse severity) /expired/superseded— computed against the prior current scan. The triage board defaults to the current scan with an Include history toggle, a Component column, lifecycle badges, and a re-failed banner. - Amendment approval + ODP validity (#809) — a disposition is a proposed amendment that only suppresses a finding once approved (by an admin or a role granted the new
amendment.approvepermission) and while inside its validity window. The window comes from the boundary profile's ODP, else a new admin Remediation Timelines SLA table (baseline × NIST criticality → days); an active POA&M for the control means no expiry. Amendments record both decider and approver, bound into the signature. - Aggregate into SSP / SAP / SAR / POA&M (#809) — an Aggregate action (synchronous or via
AggregateFindingsJob) maps findings to controls through their HDFtags.nist, writes a non-destructivehdf_scan_resultannotation on matching SSP/SAP/SAR controls, and opens POA&M items for un-suppressed failures. - Signed HDF package export (#809) — a single HMAC-SHA256 signed bundle (amendments + findings + dispositions) keyed from
SPARC_HASH, which a consumer can archive or feed downstream. FullApi::V1surface for every new action.
Also in this release: HDF Amendment Triage (#447, previously v1.14.0)
Upload scanner findings in Heimdall Data Format (a single scan or a saf convert bundle), triage each failed control into one of the seven HDF v3.4.0 override kinds (falsePositive / waiver / poam / vendorDependency / inherited / riskAdjustment / operationalRequirement) with an evidence / POA&M / attestation / risk-assessment linkage and provenance hash, then export a deterministic per-boundary HDF Amendments document your CI applies with hdf amend apply before its saf validate threshold gate. The export validates itself via hdf amend verify, and the #244 severity policy is enforced (no waiver or downgrade on CRITICAL).
Fixes shipped alongside
Found while verifying the epic end-to-end against a running instance. Each carries a regression test.
- Control-catalog XML export returned 500 for every user — OSCAL catalogs nest
<select>inside<param>, and elements are emitted withsend, so the element name resolved toKernel#selectand raisedTypeError. Also fixedhow-many, which was emitted as a child element instead of an attribute (schema-invalid OSCAL). - POA&M export was impossible — OSCAL requires
risk/statementandfinding/target; the demo data supplied neither. Fixed in the data, not the exporter: required OSCAL content is never synthesised, because a POA&M that passes schema validation while misrepresenting the risk is worse than one that fails. - Profile export was impossible for any profile containing an untailored ODP — a blank parameter produced
"values": [], which OSCAL rejects, failing the whole document. Untailored parameters are now omitted. - Markup injection on the POA&M item screens — the add/edit screens marked an interpolated, user-supplied document name
html_safe. CSP blocked script execution, so the impact is markup injection rather than working XSS, but injected elements did reach the DOM. Note Brakeman does not flag this pattern, so the suite carries the regression test. POSTwith a non-existentauthorization_boundary_idreturned 500 instead of 422 —belongs_to … optional: truemakes the association optional, not the id valid, so PostgreSQL raised an unhandled foreign-key violation. Now validated across all five document types.- A double-escaped label rendered as
POA&Mto users on the processing banner. - Navigation — Authoritative Sources had no top-nav route at all; it now appears under Enterprise › Compliance Library, mirroring the sidebar.
Naming
- Help Center guides renamed: Security Assessment Plan (SAP), Security Assessment Results (SAR), and Compliance Library (was "Trust Store").
/help/trust-storenow 301s to/help/compliance-library.
Upgrade notes
- Migrations are additive — new columns on
scan_runs,scanner_findings,finding_dispositions, plus a newremediation_timelinestable. The(boundary, control_id)unique index on findings becomes a partial index oncurrent = trueso scan history can be retained. - New permission key
amendment.approve, seeded to the Authorizing Official, Agency AO, and ISSM roles. Therolesseed section bumps to 1.1.0 so existing databases pick it up; Instance Admins bypass it regardless. SPARC_HASHkeys the signed HDF package. Without it the derivation falls back tosecret_key_base(logged in production), which weakens the cross-instance provenance claim on the bundle.- The seeded Remediation Timelines SLA grid is a starting point, not an organizational determination — review it against your own remediation policy, since those values decide how long an approved amendment suppresses a finding.
Full changelog: v1.13.2…v1.15.0
v1.13.2 — Phishing-resistant authentication enforcement
SPARC v1.13.2 — Phishing-resistant authentication enforcement
Builds on the phishing-resistant MFA foundation shipped in v1.13.0 (FIDO2) and
v1.13.1 (PIV/CAC). This release adds the enforcement layer — the controls an
operator uses to require strong authentication org-wide — plus a defense-in-depth
filter for which smart-card certificates SPARC will accept, and closes the last
raw-XML upload path.
All new controls are off by default: existing deployments upgrade with no
behavior change and no configuration required.
✨ Highlights
Mandatory FIDO2 enrollment gate (#802)
A single variable, SPARC_REQUIRE_FIDO2, both enables FIDO2 and sets who
must enroll a security key before they can use the app:
| Value | Effect |
|---|---|
off (default) |
FIDO2 opt-in only (unchanged — still enabled explicitly with SPARC_FIDO2_ENABLED=true) |
local |
Local-password users must enroll a key |
all |
Every interactive user must enroll a key |
Users without a registered key are redirected to the security-key management page
until they enroll. The break-glass admin and service accounts are exempt.
Require phishing-resistant sign-in (#805)
SPARC_REQUIRE_AUTH_METHODS is a comma-separated allowlist of the login methods
your organization accepts — e.g. oidc,piv. Anyone who authenticates by a method
not on the list is signed out and returned to the login page with a clear message.
Enforced in the app, so it works on a single-listener mTLS gateway without a
dedicated PIV host. Break-glass admin and service accounts are exempt.
Tokens: oidc (= openid_connect/sso), piv, webauthn/fido2, github,
gitlab, ldap, local. Empty (default) = no restriction.
Optional PIV/CAC certificate issuer & policy filter (#804)
Defense-in-depth on the PIV/CAC path: accept a gateway-forwarded client
certificate only if it comes from a configured issuer and/or carries a configured
certificate-policy OID.
| Variable | Meaning |
|---|---|
SPARC_PIV_ACCEPTED_ISSUERS |
CSV; case-insensitive substring match against the cert issuer DN |
SPARC_PIV_ACCEPTED_POLICY_OIDS |
CSV of certificate-policy OIDs (parsed from the cert via ASN.1) |
Both empty (default) → accept whatever the mTLS gateway validated (no change for
existing v1.13.1 PIV deployments). When set, a non-matching certificate is rejected
fail-closed and audited as a login failure; when both are set, a cert must
satisfy both. Trust of which CA to accept still lives at the gateway — this adds
a second, narrower allowlist in the app.
🔒 Security hardening
- ODP importer now parses XML through the hardened
XmlSecuritywrapper (SI-10).
This was the last file-upload path still on raw Nokogiri; it is now protected
against XXE / external-entity substitution and network fetches (.nonet),
consistent with every other XML ingestion path.
🧰 CI & test hygiene
- Ruff lint gate added for the Python test suites (#763, #801) + authentication docs.
- Gate on security-finding review-date freshness (#778, #798).
- Cleared database test-credential secret-scan false positives and fixed the
gitleaks config (#799). - CI/test-hygiene straggler cleanup (#747, #753, #788, #797).
⬆️ Upgrade notes
No action required — every new control defaults to off. To adopt them:
- Require security keys: set
SPARC_REQUIRE_FIDO2=localorall. - Require OIDC/PIV only: set
SPARC_REQUIRE_AUTH_METHODS=oidc,piv. - Restrict accepted PIV issuers/policies: set
SPARC_PIV_ACCEPTED_ISSUERSand/or
SPARC_PIV_ACCEPTED_POLICY_OIDS.
Operator setup for all three is documented in the Security Keys user guide,
which the in-app Help Center renders directly. See also docs/AUTHENTICATION.md
and docs/ENVIRONMENT_VARIABLES.md.
✅ Verification
Full tests/api (363 passed) and authenticated Playwright ui-smoke (226 passed,
including the FIDO2 enroll→passwordless ceremony) run green against the production
UBI9 image.
Full changelog: v1.13.1...v1.13.2
SPARC v1.13.1
SPARC v1.13.1
A configuration-simplification and documentation release. The headline theme is reducing what operators must set — inferring and deriving configuration instead of requiring it — hardening data-in-transit and storage posture, making PIV/CAC identity mapping configurable, and shipping the User Guides both as wiki screenshots and as an in-app Help Center.
"Not required" ≠ "deleted." Nothing that worked before stops working. These changes reduce what you must set; legacy variables remain honored as fallbacks unless explicitly called out as breaking below.
⚠️ Behavior changes to review before upgrading
- Database TLS floor (#789). In production,
sslmodeis now floored atrequire— SPARC refuses an unencrypted database connection. A boot posture check reports the measured TLS state per connection (primary + cache/queue/cable). Give SPARC a TLS-capable database, or setSPARC_DB_SSLMODEdeliberately. Seedocs/DATABASE_TLS.md. (NIST SC-8.) - Object storage defaults to local, and production hard-fails on it (#793). With nothing set, storage resolves to local disk (previously
:amazon), and production refuses to boot on local disk — because on ECS/EKS the container filesystem is ephemeral and uploads are silently lost on redeploy. SetSPARC_STORAGE_URL=s3://your-bucket(region fromAWS_REGION), or acknowledge durable local disk withSPARC_ALLOW_LOCAL_STORAGE=true. Seedocs/OBJECT_STORAGE.md. (CP-9, SI-12.) - Rate limiting is always on (#793). The
SPARC_RATE_LIMITING_ENABLEDkill switch was removed — throttling now runs unconditionally outside test. To exempt trusted sources (NLB, health checks, monitoring), safelist them withSPARC_RATE_LIMIT_SAFELIST_CIDRSinstead of disabling. - Document-approval gate defaults on (#789).
SPARC_REQUIRE_DOCUMENT_APPROVALnow defaults totrue. - Service-account inactivity window 90 → 30 days (#793).
SPARC_SA_INACTIVITY_DAYSis unified intoSPARC_INACTIVITY_DAYS(default 30); the old variable is a deprecated alias. SPARC_AWS_REGIONdeprecated (#791). Use the SDK-standardAWS_REGION; the old name still works as an alias.
Configuration simplification (#785)
Driven by the observation that a real deployment was setting ~97 env + 10 secrets, roughly half of them redundant. Three passes:
- Pass 1 (#789). Eliminate redundant/default-matching variables; infer enable-flags from credential presence (e.g. setting OIDC credentials enables OIDC — no separate toggle); derive OIDC redirect and API audience; collapse to two operator emails (admin + support). Adds structured JSON logging + log-to-stdout (default in production) for containerized log capture.
- Pass 2 (#791).
DATABASE_URLbecomes the single, preferred database source (all four databases derived from it, withSPARC_DB_*as fallback). FIDO2 relying-party ID derived from the app URL.SPARC_AWS_REGION→AWS_REGION. - Pass 2.1 (#793). Rate-limiting always-on (see above), unified inactivity window, and
SPARC_STORAGE_URLas the single object-storage variable (scheme picks the provider, the same wayDATABASE_URLdrives the database).
New/updated operator docs: docs/DATABASE_TLS.md, docs/OBJECT_STORAGE.md, docs/PIV_IDENTITY_MAPPING.md, and docs/ENVIRONMENT_VARIABLES.md.
Phishing-resistant auth — PIV/CAC identity mapping (#790)
- The PIV/CAC identity parser is now configurable rather than assuming a single DoD certificate shape.
SPARC_PIV_IDENTITY_SOURCEselects how the user identity is extracted (edipi_cn,upn,email, orsubject_cnwithSPARC_PIV_UID_PATTERN), with an email-match opt-out. UPN is now parsed from the SANotherNameOID1.3.6.1.4.1.311.20.2.3. - Proven end-to-end locally with a software-PIV + nginx mTLS proxy, exercising both the DoD and non-DoD identity shapes.
Documentation
- Per-screen screenshots in the User Guides (#781). Every User Guide now embeds a representative screenshot, captured repeatably against the prod image with real Chrome over seeded demo data (no real identifiers).
PUSH_TO_WIKI.shnow publishes image assets. - In-app Help Center (#784). Every deployment now bundles the User Guides as an in-app, offline, searchable Help Center — reachable from the sidebar, a contextual ? button that deep-links to the guide for the current screen, and the Resources page. Guides render from the same bundled Markdown, so the in-app docs and the wiki never drift.
Fixes
docker compose up --build(the standard dev command) is fixed — the storage/DB-TLS posture checks no longer fire duringassets:precompile(a build step), which had been aborting the image build.
Full config disposition and rationale: docs/dev/785_config_reduction_plan.md.
v1.13.0 — FIDO2 + PIV authentication (app-native MFA)
Phishing-resistant, DoD-ready authentication — SPARC's first app-native multi-factor authentication. A security key or a CAC/PIV smart card + PIN is now a complete, single-step login.
Highlights
FIDO2 / WebAuthn passwordless sign-in (#779)
Register a FIDO2 security key (YubiKey, Feitian, Token2, a platform authenticator, …) with a PIN, then sign in with just the key — no password. The key + PIN is possession and knowledge in one ceremony, so it satisfies MFA on its own, and it's phishing- and replay-resistant. Authenticator-agnostic: resident and non-resident keys both work, usernameless with an email-first fallback. Users manage their keys from a Security Keys page; lockout recovery is an admin key-reset (no self-service codes by design). Enable with SPARC_FIDO2_ENABLED.
PIV / CAC smart-card sign-in (#779)
SPARC accepts a DoD PIV/CAC certificate — delivering NIST IA-2(12) "Acceptance of PIV Credentials". The mutual-TLS handshake, DoD PKI validation, and revocation happen at the gateway (ALB/nginx); SPARC consumes the already-validated certificate it forwards, maps the EDIPI/email to a user, and fails closed unless the gateway attests verification. Enable with SPARC_ENABLE_PIV. The gateway/mTLS setup is tracked in sparc-iac#559.
FIDO U2F
Covered for free — a WebAuthn implementation accepts a U2F key as a non-resident second factor.
Compliance
- IA-2(1)/(2) — app-native MFA via FIDO2 (no longer conditional on an external OIDC IdP).
- IA-2(8) — WebAuthn is replay- and phishing-resistant (per-ceremony challenge, origin binding, signature-counter clone detection).
- IA-2(12) — native PIV/CAC acceptance → Implemented.
NIST mapping, the authentication CDEF, and new wiki guides (Authentication and MFA, User Guide: Security Keys & Smart Cards) are all updated.
Upgrade notes
- No behavior change unless you opt in —
SPARC_FIDO2_ENABLEDandSPARC_ENABLE_PIVboth default tofalse. - FIDO2: set
SPARC_APP_URLto the externally-visible origin (https://…) — WebAuthn binds credentials to it, so a mismatch fails only in that environment. - PIV: requires the mTLS gateway from sparc-iac#559; SPARC fails closed without it. See the header contract in the Authentication and MFA guide.
- The
opensslgem is pinned~> 3.3— parity with the version the prod image already ships (its OpenSSL library remains 3.x); not a crypto change. - No database migration is required beyond the automatic
webauthn_credentialstable.
Full changelog: v1.12.3...v1.13.0
v1.12.3 — Outbound-TLS trust (LDAP verify · custom-CA · proxy egress)
Outbound-TLS trust cluster — SPARC can now be deployed in a locked-down enterprise or DoD environment (private CAs, a mandated TLS egress proxy, a verified LDAPS directory) without weakening any verification. Closes the three gaps found by the v1.12.2 outbound-TLS audit.
Highlights
LDAP verifies the directory server certificate (#773)
simple_tls/start_tls previously encrypted the channel but never authenticated the directory server's cert (net-ldap defaults to VERIFY_NONE when tls_options is omitted) — leaving LDAP bind credentials MITM-open. LDAP now verifies with VERIFY_PEER by default. Supply the directory CA via SPARC_LDAP_CA_FILE or the container trust store; SPARC_LDAP_TLS_VERIFY=false is an env-gated, loudly-logged opt-out for legacy internal directories.
Custom / private-CA trust for the container (#774)
Two ways to trust a private, corporate-proxy, or DoD-PKI CA, both folding into the OpenSSL trust store so every outbound client benefits (Net::HTTP, RestClient, AWS SDK, and the LDAP default store):
- Runtime, no rebuild: mount CAs and point
SPARC_EXTRA_CA_CERTSat them (default/rails/certs); the entrypoint appends them to the system bundle and exportsSSL_CERT_FILEas the non-root runtime user (public CAs stay trusted). - Build-time bake-in: drop PEMs into
certs/→update-ca-trust. For locked-down / air-gapped image builds.
Outbound HTTP honors the egress proxy (#775)
Ruby's Net::HTTP :ENV default reads http_proxy only and ignores https_proxy for every scheme — so a deployment setting the conventional HTTPS_PROXY had all of SPARC's (HTTPS) egress bypass the proxy. Outbound HTTP is now centralized through SparcHttp, routing via URI#find_proxy — scheme-strict (https→https_proxy), NO_PROXY-aware, VERIFY_PEER.
Together, #774 + #775 support an operator-sanctioned TLS-inspecting egress proxy or private PKI without ever disabling verification: SPARC still verifies every connection, but against a CA the operator explicitly installed — an untrusted interceptor is still rejected.
Also included
- Fail-closed TLS check (
tests/ui-smoke/test_tls_verification.py): asserts an untrusted cert is rejected when verification is on (not just that a valid one is accepted). - ui-smoke harness now honors the TLS-verify mode across all transports (enables release-grade smoke against the local prod image over self-signed TLS).
Upgrade notes
- LDAP (#773) — breaking: deployments using
start_tls/simple_tlswith a self-signed / private directory cert must now add the CA (SPARC_LDAP_CA_FILEor the #774 container trust) or setSPARC_LDAP_TLS_VERIFY=false(insecure). No change for LDAPS with a publicly-trusted cert. - Proxy (#775): if you relied on
http_proxycovering HTTPS egress, also setHTTPS_PROXY(now honored scheme-strictly). - New env vars:
SPARC_EXTRA_CA_CERTS,SPARC_LDAP_CA_FILE,SPARC_LDAP_TLS_VERIFY, plusHTTPS_PROXY/HTTP_PROXY/NO_PROXY(now honored). No migration required.
Full changelog: v1.12.2...v1.12.3
v1.12.2 — Evidence API, hdf-cli 3.4.1, boundary/organization management
SPARC v1.12.2 — Evidence API, hdf-cli 3.4.1, and boundary/organization management
A feature + fix patch release bundling eight PRs on top of v1.12.1.
Highlights
Evidence API (#756)
Evidence is now fully manageable over REST — create, update, file-upload, and associate with controls — closing the gap where evidence was UI-only and the API could attest against records it had no way to create. Document-scoped control links drive OSCAL back-matter, referencing the artifact by its durable /artifacts/:uuid resolver URL. Uploads accept arbitrary artifact content, guarded by an executable-signature deny-list plus the size cap (NIST SI-10); collected_at/collected_by remain system-recorded (AU-10).
hdf-cli 3.2.0 → 3.4.1 (#764)
Moves the HDF↔OSCAL translation bridge off two silent data-corruption defects in the older CLI: fabricated POA&M expiry dates and conversion-time-stamped-as-assessment-time SARs. Removes the baselines injection workaround (fixed upstream in 3.3.1) — which on 3.4.1 was the only thing letting non-HDF input through, so removing it strengthens input validation. Surfaces the new "POA&M requires a deadline" failure as an actionable 422.
Boundary & organization management (#770)
- Configurable environments —
SPARC_ENVIRONMENTS_LIST(Name:CODEpairs), defaulting to the six standard RMF environments (DEV/TEST/STAG/UAT/QA/PROD). Previously a hardcoded 4-value set missing UAT and QA. - Unified personnel roster — the boundary screen now shows personnel assigned through both the admin screen (canonical roles) and the boundary screen (legacy memberships); previously only one path was visible (AC-3).
- Organization ↔ boundary association — associate a boundary with an organization from the admin org screen, with one authorization matrix shared by the UI and a new API endpoint: instance admin may move between orgs; a non-admin org admin may attach an unassigned boundary into their own org only (AC-3/AC-6).
- Three UI fixes: a filter on the environment form's CDEF picker, uniform clickable Artifact Summary tiles, and boundary-scoped artifact management surfaced on the boundary screen.
SonarCloud reliability (#762)
Clears the actionable half of the reliability backlog — nine fixes (bounded ReDoS in the STIG parser, explicit ENV.fetch, keyboard-operable summary chips, table row headers), with the remaining findings triaged as false positives or won't-fix.
Admin: Create User (#755)
Adds "Create User" to the admin UI over the shared provisioning service.
Security & dependencies
- rails-html-sanitizer 1.7.0 → 1.7.1 (#767) — remediates an XSS advisory (GHSA-cj75-f6xr-r4g7) in the gem SPARC uses for HTML sanitization.
- Scanner-findings audit reconciled against the UBI9 prod image (#777) — every CVE disposition re-verified against a live Trivy + Grype scan; mislabeled entries re-triaged, obsolete Debian-era suppressions removed. Release verification now runs against the UBI9 production image. Follow-ups filed for outbound-TLS trust gaps (#773–775), an hdf-libs Go-stdlib bump (#776), and automated review-date enforcement (#778).
- GitHub Actions pins (#769) and AWS SDK minor bumps (#768).
Also included
- API tooling (#759):
sparc-apihelper CLI; fixes a stale Postman prod URL. - Docs (#754, #761): wiki-canonical consolidation; README cleanup.
Upgrade notes
- No migration required. The environment change stores the name slug, so existing
production/development/staging/testvalues round-trip. - New env var:
SPARC_ENVIRONMENTS_LIST(optional; defaults to the six RMF environments). See Configuration. - hdf-cli in the image is now 3.4.1; the translation bridge pins to it. POA&M inputs to the amendments path must now carry
risks[].deadline(previously a date was fabricated).
Full changelog: v1.12.1...v1.12.2
v1.12.1 — UBI9 UTF-8 locale hotfix
Hotfix — UTF-8 locale in the UBI9 image (#750)
Fixes the v1.12.0 regression where the UBI9 image returned HTTP 500 on all full-layout pages (including /login) when SPARC_HEADER_TEXT (the rules-of-behavior header) — or any operator env var rendered into a page, incl. the consent banner — contained non-ASCII characters.
- Root cause: UBI9 minimal ships no UTF-8 locale → Ruby tags
ENV[]values as BINARY (ASCII-8BIT) → rendering them into the UTF-8 layout raisedEncoding::CompatibilityError. The prior Debian base setLANG=C.UTF-8implicitly, masking it. - Fix:
ENV LANG=C.UTF-8/LC_ALL=C.UTF-8in the image (both build stages) + a build-time guard assertingEncoding.default_external == UTF-8so it can't silently regress. - No action needed on upgrade — non-ASCII
SPARC_HEADER_TEXT/ banner text renders correctly. - Also: digest-only base-image reference + Dockerfile lint cleanup (SonarQube docker:S6596 / S103).
Reproduced on the real v1.12.0 image (500) and validated fixed (200). Tracking: #750.
Full changelog: v1.12.0...v1.12.1