@growth-labs/conformance 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +64 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/policy.d.ts +33 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +91 -5
- package/dist/policy.js.map +1 -1
- package/dist/runner.js +74 -4
- package/dist/runner.js.map +1 -1
- package/dist/schema.d.ts +326 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +75 -0
- package/dist/schema.js.map +1 -1
- package/fixtures/negative/required-negative-fixtures.json +11 -0
- package/fixtures/valid/public-article.json +1 -1
- package/package.json +12 -1
- package/src/index.ts +6 -0
- package/src/policy.ts +138 -6
- package/src/runner.ts +93 -4
- package/src/schema.ts +83 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @growth-labs/conformance Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 — 2026-07-17
|
|
4
|
+
|
|
5
|
+
- Mark the `astro` peer dependency as optional (`peerDependenciesMeta.astro.optional: true`).
|
|
6
|
+
Non-Astro tooling consumers are no longer burdened with Astro's native/tooling
|
|
7
|
+
graph during install. The compatible peer range (`^7.1.0`) is retained for Astro
|
|
8
|
+
consumers, and the sole Astro reference in source (`import type { AstroConfig }`)
|
|
9
|
+
remains a type-only import that is erased at compile time.
|
|
10
|
+
|
|
11
|
+
## 0.3.0 — 2026-07-17
|
|
12
|
+
|
|
13
|
+
- Add `buildAstroCspConfig()` for Astro 7.1+ native SHA-256 CSP generation with
|
|
14
|
+
exact external-origin validation and no wildcard or `unsafe-*` escape hatch.
|
|
15
|
+
Its return shape is compile-checked against Astro's exported CSP config type,
|
|
16
|
+
and a real Astro build proves the attribute-only directive is emitted.
|
|
17
|
+
- Allow Astro 7.1+ consumers with a proven inline-style-attribute inventory to
|
|
18
|
+
scope `'unsafe-inline'` to `style-src-attr` only. Script and style-element
|
|
19
|
+
execution remain hash-protected and fail closed. The runner requires the
|
|
20
|
+
inventory receipt to match the exact build commit. The policy API requires
|
|
21
|
+
that structured receipt rather than a boolean escape hatch, including a
|
|
22
|
+
versioned scanner identity and SHA-256 output hash.
|
|
23
|
+
- Require canonical lowercase commit SHAs, SHA-256 evidence hashes, and CSP
|
|
24
|
+
hash algorithm tokens in security evidence.
|
|
25
|
+
- Let consumers set `contentSecurityPolicy: false` on
|
|
26
|
+
`buildSharedPublicationHeaders()` when Astro owns the per-response hash CSP;
|
|
27
|
+
all other shared response headers remain package-owned.
|
|
28
|
+
- Add an explicit per-fixture frame policy contract. The default remains
|
|
29
|
+
`frame-ancestors 'none'` plus `X-Frame-Options: DENY`; deliberately embeddable
|
|
30
|
+
routes may declare only `'self'`, `https:`, or exact HTTPS origins and must
|
|
31
|
+
omit a conflicting legacy frame header. The runner validates the CSP sources
|
|
32
|
+
exactly rather than accepting a fleet-wide relaxation.
|
|
33
|
+
|
|
3
34
|
## 0.2.0 — 2026-07-16
|
|
4
35
|
|
|
5
36
|
### Added
|
package/README.md
CHANGED
|
@@ -45,6 +45,10 @@ const fixture = {
|
|
|
45
45
|
canonicalUrl: 'https://fronts.co/example',
|
|
46
46
|
schemaTypes: ['NewsArticle'],
|
|
47
47
|
discoverEligible: true,
|
|
48
|
+
framePolicy: {
|
|
49
|
+
ancestors: ["'none'"],
|
|
50
|
+
legacyHeader: 'DENY',
|
|
51
|
+
},
|
|
48
52
|
},
|
|
49
53
|
publicationSurfaces: [
|
|
50
54
|
{ name: 'sitemap', status: 'present', httpStatus: 200, bodyBytes: 1024 },
|
|
@@ -72,10 +76,18 @@ const fixture = {
|
|
|
72
76
|
evidence: 'sanitized CSP report or blocked-canary receipt',
|
|
73
77
|
},
|
|
74
78
|
],
|
|
79
|
+
inlineStyleAttributes: {
|
|
80
|
+
commitSha: 'bd39d718f6a5b0df9a249e11638a8b430050941e',
|
|
81
|
+
occurrenceCount: 4,
|
|
82
|
+
evidence: {
|
|
83
|
+
tool: 'platform-foundations:inline-style-inventory@1',
|
|
84
|
+
outputHash: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
75
87
|
},
|
|
76
88
|
buildEvidence: {
|
|
77
89
|
commitSha: 'bd39d718f6a5b0df9a249e11638a8b430050941e',
|
|
78
|
-
packageVersions: { '@growth-labs/conformance': '0.
|
|
90
|
+
packageVersions: { '@growth-labs/conformance': '0.3.0', '@growth-labs/seo': '0.9.0' },
|
|
79
91
|
staticHeaderPolicy: true,
|
|
80
92
|
ssrHeaderPolicy: true,
|
|
81
93
|
},
|
|
@@ -88,20 +100,67 @@ const fleetBundle = runFleetConformance([fixture])
|
|
|
88
100
|
## Shared Header Policy
|
|
89
101
|
|
|
90
102
|
Use `buildSharedPublicationHeaders()` when a site wants the package-owned
|
|
91
|
-
baseline for SSR and static response headers
|
|
103
|
+
baseline for SSR and static response headers. Astro 7.1+ sites should let Astro
|
|
104
|
+
own the per-route hash CSP and retain the remaining shared headers. The helper's
|
|
105
|
+
peer contract is intentionally pinned to the first Astro release that supports
|
|
106
|
+
attribute-scoped style resources:
|
|
92
107
|
|
|
93
108
|
```ts
|
|
94
|
-
import {
|
|
109
|
+
import {
|
|
110
|
+
buildAstroCspConfig,
|
|
111
|
+
buildSharedPublicationHeaders,
|
|
112
|
+
} from '@growth-labs/conformance/policy'
|
|
113
|
+
|
|
114
|
+
const csp = buildAstroCspConfig({
|
|
115
|
+
scriptSources: ['https://challenges.cloudflare.com'],
|
|
116
|
+
styleSources: ['https://fonts.googleapis.com'],
|
|
117
|
+
inlineStyleAttributes: {
|
|
118
|
+
commitSha: 'bd39d718f6a5b0df9a249e11638a8b430050941e',
|
|
119
|
+
occurrenceCount: 4,
|
|
120
|
+
evidence: {
|
|
121
|
+
tool: 'platform-foundations:inline-style-inventory@1',
|
|
122
|
+
outputHash: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
})
|
|
95
126
|
|
|
96
127
|
const headers = buildSharedPublicationHeaders({
|
|
97
128
|
renderMode: 'ssr',
|
|
98
129
|
audience: 'public',
|
|
99
|
-
|
|
100
|
-
cspReportUri: 'https://example.com/api/csp-report',
|
|
130
|
+
contentSecurityPolicy: false,
|
|
101
131
|
markdownTwinUrl: 'https://example.com/article.md',
|
|
102
132
|
})
|
|
103
133
|
```
|
|
104
134
|
|
|
135
|
+
The fixture default is the non-embeddable policy above. For a deliberately
|
|
136
|
+
embeddable route, declare its exact response class and omit a conflicting
|
|
137
|
+
`X-Frame-Options` header:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
expected: {
|
|
141
|
+
framePolicy: {
|
|
142
|
+
ancestors: ['https://trusted.example'],
|
|
143
|
+
legacyHeader: 'omit',
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Accepted ancestors are `'none'`, `'self'`, `https:`, and exact HTTPS origins;
|
|
149
|
+
wildcards, paths, credentials, query strings, and fragments are rejected.
|
|
150
|
+
`DENY` is valid only with `'none'`, and `SAMEORIGIN` only with `'self'`; broader
|
|
151
|
+
embeddable policies must use `legacyHeader: 'omit'` to avoid contradictory
|
|
152
|
+
browser behavior.
|
|
153
|
+
|
|
154
|
+
Astro 7.1+ consumers that prove they still emit HTML `style` attributes may set
|
|
155
|
+
`inlineStyleAttributes` to the validated inventory receipt. There is no boolean
|
|
156
|
+
escape hatch. The helper then scopes `'unsafe-inline'` to `style-src-attr`
|
|
157
|
+
while preserving hash protection for scripts and `<style>` elements. The
|
|
158
|
+
receipt requires the exact lowercase 40-character build commit SHA, a positive
|
|
159
|
+
occurrence count, the versioned scanner identity, and the lowercase SHA-256
|
|
160
|
+
content hash of its machine output. The same receipt must appear in the
|
|
161
|
+
conformance fixture and its commit must match `buildEvidence.commitSha`. Do not
|
|
162
|
+
enable this option as a generic compatibility fix.
|
|
163
|
+
|
|
105
164
|
The policy emits CSP, HSTS, frame protection, MIME sniffing protection,
|
|
106
165
|
COOP, CORP, referrer policy, permissions policy, cache semantics, and optional
|
|
107
166
|
markdown twin `Link` metadata. Private audiences use `private, no-store`;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { buildCacheControl, buildContentSecurityPolicy, buildSharedPublicationHeaders, buildStrictTransportSecurity, type CachePolicyOptions, type ContentSecurityPolicyOptions, type CrossOriginOpenerPolicy, type CrossOriginResourcePolicy, collectHeaders, type HeaderPolicyOptions, isPrivateAudience, normalizeHeaders, REQUIRED_SECURITY_HEADERS, type StrictTransportSecurityOptions, } from './policy.js';
|
|
1
|
+
export { type AstroCspConfig, type AstroCspPolicyOptions, type AstroCspResource, buildAstroCspConfig, buildCacheControl, buildContentSecurityPolicy, buildSharedPublicationHeaders, buildStrictTransportSecurity, type CachePolicyOptions, type ContentSecurityPolicyOptions, type CrossOriginOpenerPolicy, type CrossOriginResourcePolicy, collectHeaders, type HeaderPolicyOptions, isPrivateAudience, normalizeHeaders, REQUIRED_SECURITY_HEADERS, type StrictTransportSecurityOptions, } from './policy.js';
|
|
2
2
|
export { type RunConformanceOptions, runFleetConformance, runSiteConformance } from './runner.js';
|
|
3
|
-
export { type Audience, audienceSchema, type BuildEvidence, buildEvidenceSchema, type CacheVariant, type ConformanceCheckReceipt, type ConformanceFinding, type ConformanceFixture, type ConformanceFixtureInput, type CspReportReceipt, cacheVariantSchema, checkStatusSchema, conformanceCheckReceiptSchema, conformanceExpectedSchema, conformanceFindingSchema, conformanceFixtureSchema, cspReportReceiptSchema, EVIDENCE_SCHEMA_VERSION, type EvidenceSummary, evidenceSummarySchema, type FleetEvidenceBundle, findingSeveritySchema, fleetEvidenceBundleSchema, type HeaderMap, headerMapSchema, headerValueSchema, type PublicationSurfaceEvidence, parseConformanceFixture, publicationSurfaceSchema, REQUIRED_FUNCTIONAL_PROBE_NAMES, REQUIRED_NEGATIVE_FIXTURE_CODES, type RenderMode, type RequiredNegativeFindingCode, type RequiredNegativeFixtureId, renderModeSchema, type SecurityFunctionalProbeReceipt, type SecurityReceipts, type SiteEvidenceBundle, type SurfaceResponse, securityFunctionalProbeReceiptSchema, securityReceiptsSchema, siteEvidenceBundleSchema, surfaceResponseSchema, validateFleetEvidenceBundle, validateSiteEvidenceBundle, } from './schema.js';
|
|
3
|
+
export { type Audience, audienceSchema, type BuildEvidence, buildEvidenceSchema, type CacheVariant, type ConformanceCheckReceipt, type ConformanceFinding, type ConformanceFixture, type ConformanceFixtureInput, type CspReportReceipt, cacheVariantSchema, checkStatusSchema, conformanceCheckReceiptSchema, conformanceExpectedSchema, conformanceFindingSchema, conformanceFixtureSchema, cspReportReceiptSchema, EVIDENCE_SCHEMA_VERSION, type EvidenceSummary, evidenceSummarySchema, type FleetEvidenceBundle, findingSeveritySchema, fleetEvidenceBundleSchema, type HeaderMap, headerMapSchema, headerValueSchema, type InlineStyleAttributeInventory, inlineStyleAttributeInventorySchema, type PublicationSurfaceEvidence, parseConformanceFixture, publicationSurfaceSchema, REQUIRED_FUNCTIONAL_PROBE_NAMES, REQUIRED_NEGATIVE_FIXTURE_CODES, type RenderMode, type RequiredNegativeFindingCode, type RequiredNegativeFixtureId, renderModeSchema, type SecurityFunctionalProbeReceipt, type SecurityReceipts, type SiteEvidenceBundle, type SurfaceResponse, securityFunctionalProbeReceiptSchema, securityReceiptsSchema, siteEvidenceBundleSchema, surfaceResponseSchema, validateFleetEvidenceBundle, validateSiteEvidenceBundle, } from './schema.js';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,iBAAiB,EACjB,0BAA0B,EAC1B,6BAA6B,EAC7B,4BAA4B,EAC5B,KAAK,kBAAkB,EACvB,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,cAAc,EACd,KAAK,mBAAmB,EACxB,iBAAiB,EACjB,gBAAgB,EAChB,yBAAyB,EACzB,KAAK,8BAA8B,GACnC,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,KAAK,qBAAqB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACjG,OAAO,EACN,KAAK,QAAQ,EACb,cAAc,EACd,KAAK,aAAa,EAClB,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,6BAA6B,EAC7B,yBAAyB,EACzB,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,KAAK,eAAe,EACpB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,KAAK,SAAS,EACd,eAAe,EACf,iBAAiB,EACjB,KAAK,0BAA0B,EAC/B,uBAAuB,EACvB,wBAAwB,EACxB,+BAA+B,EAC/B,+BAA+B,EAC/B,KAAK,UAAU,EACf,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,EAC9B,gBAAgB,EAChB,KAAK,8BAA8B,EACnC,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,oCAAoC,EACpC,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,GAC1B,MAAM,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,0BAA0B,EAC1B,6BAA6B,EAC7B,4BAA4B,EAC5B,KAAK,kBAAkB,EACvB,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,cAAc,EACd,KAAK,mBAAmB,EACxB,iBAAiB,EACjB,gBAAgB,EAChB,yBAAyB,EACzB,KAAK,8BAA8B,GACnC,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,KAAK,qBAAqB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACjG,OAAO,EACN,KAAK,QAAQ,EACb,cAAc,EACd,KAAK,aAAa,EAClB,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,6BAA6B,EAC7B,yBAAyB,EACzB,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,KAAK,eAAe,EACpB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,KAAK,SAAS,EACd,eAAe,EACf,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,mCAAmC,EACnC,KAAK,0BAA0B,EAC/B,uBAAuB,EACvB,wBAAwB,EACxB,+BAA+B,EAC/B,+BAA+B,EAC/B,KAAK,UAAU,EACf,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,EAC9B,gBAAgB,EAChB,KAAK,8BAA8B,EACnC,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,oCAAoC,EACpC,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,GAC1B,MAAM,aAAa,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { buildCacheControl, buildContentSecurityPolicy, buildSharedPublicationHeaders, buildStrictTransportSecurity, collectHeaders, isPrivateAudience, normalizeHeaders, REQUIRED_SECURITY_HEADERS, } from './policy.js';
|
|
1
|
+
export { buildAstroCspConfig, buildCacheControl, buildContentSecurityPolicy, buildSharedPublicationHeaders, buildStrictTransportSecurity, collectHeaders, isPrivateAudience, normalizeHeaders, REQUIRED_SECURITY_HEADERS, } from './policy.js';
|
|
2
2
|
export { runFleetConformance, runSiteConformance } from './runner.js';
|
|
3
|
-
export { audienceSchema, buildEvidenceSchema, cacheVariantSchema, checkStatusSchema, conformanceCheckReceiptSchema, conformanceExpectedSchema, conformanceFindingSchema, conformanceFixtureSchema, cspReportReceiptSchema, EVIDENCE_SCHEMA_VERSION, evidenceSummarySchema, findingSeveritySchema, fleetEvidenceBundleSchema, headerMapSchema, headerValueSchema, parseConformanceFixture, publicationSurfaceSchema, REQUIRED_FUNCTIONAL_PROBE_NAMES, REQUIRED_NEGATIVE_FIXTURE_CODES, renderModeSchema, securityFunctionalProbeReceiptSchema, securityReceiptsSchema, siteEvidenceBundleSchema, surfaceResponseSchema, validateFleetEvidenceBundle, validateSiteEvidenceBundle, } from './schema.js';
|
|
3
|
+
export { audienceSchema, buildEvidenceSchema, cacheVariantSchema, checkStatusSchema, conformanceCheckReceiptSchema, conformanceExpectedSchema, conformanceFindingSchema, conformanceFixtureSchema, cspReportReceiptSchema, EVIDENCE_SCHEMA_VERSION, evidenceSummarySchema, findingSeveritySchema, fleetEvidenceBundleSchema, headerMapSchema, headerValueSchema, inlineStyleAttributeInventorySchema, parseConformanceFixture, publicationSurfaceSchema, REQUIRED_FUNCTIONAL_PROBE_NAMES, REQUIRED_NEGATIVE_FIXTURE_CODES, renderModeSchema, securityFunctionalProbeReceiptSchema, securityReceiptsSchema, siteEvidenceBundleSchema, surfaceResponseSchema, validateFleetEvidenceBundle, validateSiteEvidenceBundle, } from './schema.js';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,mBAAmB,EACnB,iBAAiB,EACjB,0BAA0B,EAC1B,6BAA6B,EAC7B,4BAA4B,EAK5B,cAAc,EAEd,iBAAiB,EACjB,gBAAgB,EAChB,yBAAyB,GAEzB,MAAM,aAAa,CAAA;AACpB,OAAO,EAA8B,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACjG,OAAO,EAEN,cAAc,EAEd,mBAAmB,EAOnB,kBAAkB,EAClB,iBAAiB,EACjB,6BAA6B,EAC7B,yBAAyB,EACzB,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EAEvB,qBAAqB,EAErB,qBAAqB,EACrB,yBAAyB,EAEzB,eAAe,EACf,iBAAiB,EAEjB,mCAAmC,EAEnC,uBAAuB,EACvB,wBAAwB,EACxB,+BAA+B,EAC/B,+BAA+B,EAI/B,gBAAgB,EAKhB,oCAAoC,EACpC,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,GAC1B,MAAM,aAAa,CAAA"}
|
package/dist/policy.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AstroConfig } from 'astro';
|
|
2
|
+
import { type Audience, type HeaderMap, type InlineStyleAttributeInventory, type RenderMode } from './schema.js';
|
|
2
3
|
export declare const REQUIRED_SECURITY_HEADERS: readonly ["content-security-policy", "strict-transport-security", "cross-origin-opener-policy", "cross-origin-resource-policy", "x-frame-options", "x-content-type-options", "referrer-policy", "permissions-policy"];
|
|
3
4
|
export type CrossOriginOpenerPolicy = 'same-origin' | 'same-origin-allow-popups' | 'unsafe-none';
|
|
4
5
|
export type CrossOriginResourcePolicy = 'same-origin' | 'same-site' | 'cross-origin';
|
|
@@ -18,20 +19,51 @@ export interface HeaderPolicyOptions {
|
|
|
18
19
|
nonce?: string;
|
|
19
20
|
cspReportUri?: string;
|
|
20
21
|
cspReportTo?: string;
|
|
22
|
+
contentSecurityPolicy?: false | ContentSecurityPolicyOptions;
|
|
21
23
|
strictTransportSecurity?: StrictTransportSecurityOptions;
|
|
22
24
|
crossOriginOpenerPolicy?: CrossOriginOpenerPolicy;
|
|
23
25
|
crossOriginResourcePolicy?: CrossOriginResourcePolicy;
|
|
24
26
|
markdownTwinUrl?: string;
|
|
25
27
|
}
|
|
28
|
+
export interface AstroCspPolicyOptions {
|
|
29
|
+
scriptSources?: readonly string[];
|
|
30
|
+
styleSources?: readonly string[];
|
|
31
|
+
inlineStyleAttributes?: InlineStyleAttributeInventory;
|
|
32
|
+
imgSources?: readonly string[];
|
|
33
|
+
fontSources?: readonly string[];
|
|
34
|
+
connectSources?: readonly string[];
|
|
35
|
+
mediaSources?: readonly string[];
|
|
36
|
+
frameSources?: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
type AstroNativeCspConfig = Exclude<NonNullable<NonNullable<AstroConfig['security']>['csp']>, boolean>;
|
|
39
|
+
type AstroCspDirective = NonNullable<AstroNativeCspConfig['directives']>[number];
|
|
40
|
+
export type AstroCspResource = NonNullable<NonNullable<AstroNativeCspConfig['styleDirective']>['resources']>[number];
|
|
41
|
+
export interface AstroCspConfig {
|
|
42
|
+
algorithm: 'SHA-256';
|
|
43
|
+
directives: AstroCspDirective[];
|
|
44
|
+
scriptDirective: {
|
|
45
|
+
resources: string[];
|
|
46
|
+
};
|
|
47
|
+
styleDirective: {
|
|
48
|
+
resources: AstroCspResource[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
26
51
|
export interface CachePolicyOptions {
|
|
27
52
|
renderMode: RenderMode;
|
|
28
53
|
audience: Audience;
|
|
29
54
|
}
|
|
30
55
|
export declare function buildSharedPublicationHeaders(options: HeaderPolicyOptions): Record<string, string>;
|
|
56
|
+
/**
|
|
57
|
+
* Build framework-owned CSP configuration for Astro 7.1+.
|
|
58
|
+
* Astro hashes every script and style it emits, while this helper keeps the
|
|
59
|
+
* consumer's external source inventory exact and fail-closed.
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildAstroCspConfig(options?: AstroCspPolicyOptions): AstroCspConfig;
|
|
31
62
|
export declare function buildContentSecurityPolicy(input?: string | ContentSecurityPolicyOptions): string;
|
|
32
63
|
export declare function buildStrictTransportSecurity(options?: StrictTransportSecurityOptions): string;
|
|
33
64
|
export declare function buildCacheControl({ renderMode, audience }: CachePolicyOptions): string;
|
|
34
65
|
export declare function normalizeHeaders(headers: HeaderMap | undefined): Record<string, string>;
|
|
35
66
|
export declare function collectHeaders(headers: HeaderMap | undefined): Record<string, string[]>;
|
|
36
67
|
export declare function isPrivateAudience(audience: Audience): boolean;
|
|
68
|
+
export {};
|
|
37
69
|
//# sourceMappingURL=policy.d.ts.map
|
package/dist/policy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
|
package/dist/policy.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inlineStyleAttributeInventorySchema, } from './schema.js';
|
|
1
2
|
export const REQUIRED_SECURITY_HEADERS = [
|
|
2
3
|
'content-security-policy',
|
|
3
4
|
'strict-transport-security',
|
|
@@ -10,11 +11,6 @@ export const REQUIRED_SECURITY_HEADERS = [
|
|
|
10
11
|
];
|
|
11
12
|
export function buildSharedPublicationHeaders(options) {
|
|
12
13
|
const headers = {
|
|
13
|
-
'Content-Security-Policy': buildContentSecurityPolicy({
|
|
14
|
-
nonce: options.nonce,
|
|
15
|
-
reportUri: options.cspReportUri,
|
|
16
|
-
reportTo: options.cspReportTo,
|
|
17
|
-
}),
|
|
18
14
|
'Strict-Transport-Security': buildStrictTransportSecurity(options.strictTransportSecurity),
|
|
19
15
|
'Cross-Origin-Opener-Policy': options.crossOriginOpenerPolicy ?? 'same-origin',
|
|
20
16
|
'Cross-Origin-Resource-Policy': options.crossOriginResourcePolicy ?? 'same-origin',
|
|
@@ -24,11 +20,101 @@ export function buildSharedPublicationHeaders(options) {
|
|
|
24
20
|
'Permissions-Policy': 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()',
|
|
25
21
|
'Cache-Control': buildCacheControl(options),
|
|
26
22
|
};
|
|
23
|
+
if (options.contentSecurityPolicy !== false) {
|
|
24
|
+
headers['Content-Security-Policy'] = buildContentSecurityPolicy(options.contentSecurityPolicy ?? {
|
|
25
|
+
nonce: options.nonce,
|
|
26
|
+
reportUri: options.cspReportUri,
|
|
27
|
+
reportTo: options.cspReportTo,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
27
30
|
if (options.markdownTwinUrl) {
|
|
28
31
|
headers.Link = `<${options.markdownTwinUrl}>; rel="alternate"; type="text/markdown"`;
|
|
29
32
|
}
|
|
30
33
|
return headers;
|
|
31
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Build framework-owned CSP configuration for Astro 7.1+.
|
|
37
|
+
* Astro hashes every script and style it emits, while this helper keeps the
|
|
38
|
+
* consumer's external source inventory exact and fail-closed.
|
|
39
|
+
*/
|
|
40
|
+
export function buildAstroCspConfig(options = {}) {
|
|
41
|
+
const inlineStyleAttributes = options.inlineStyleAttributes
|
|
42
|
+
? inlineStyleAttributeInventorySchema.parse(options.inlineStyleAttributes)
|
|
43
|
+
: undefined;
|
|
44
|
+
const scriptSources = exactCspSources(options.scriptSources);
|
|
45
|
+
const styleSources = exactCspSources(options.styleSources);
|
|
46
|
+
const imgSources = exactCspSources(options.imgSources);
|
|
47
|
+
const fontSources = exactCspSources(options.fontSources);
|
|
48
|
+
const connectSources = exactCspSources(options.connectSources);
|
|
49
|
+
const mediaSources = exactCspSources(options.mediaSources);
|
|
50
|
+
const frameSources = exactCspSources(options.frameSources);
|
|
51
|
+
return {
|
|
52
|
+
algorithm: 'SHA-256',
|
|
53
|
+
directives: [
|
|
54
|
+
"default-src 'self'",
|
|
55
|
+
cspDirective('img-src', ["'self'", 'data:', 'blob:', ...imgSources]),
|
|
56
|
+
cspDirective('font-src', ["'self'", 'data:', ...fontSources]),
|
|
57
|
+
cspDirective('connect-src', ["'self'", ...connectSources]),
|
|
58
|
+
cspDirective('media-src', ["'self'", 'blob:', ...mediaSources]),
|
|
59
|
+
cspDirective('frame-src', ["'self'", ...frameSources]),
|
|
60
|
+
"object-src 'none'",
|
|
61
|
+
"base-uri 'self'",
|
|
62
|
+
"frame-ancestors 'none'",
|
|
63
|
+
"form-action 'self'",
|
|
64
|
+
'upgrade-insecure-requests',
|
|
65
|
+
],
|
|
66
|
+
scriptDirective: {
|
|
67
|
+
resources: uniqueCspSources(["'self'", ...scriptSources]),
|
|
68
|
+
},
|
|
69
|
+
styleDirective: {
|
|
70
|
+
resources: inlineStyleAttributes
|
|
71
|
+
? [
|
|
72
|
+
...uniqueCspSources(["'self'", ...styleSources]).map((resource) => ({
|
|
73
|
+
resource,
|
|
74
|
+
kind: 'element',
|
|
75
|
+
})),
|
|
76
|
+
{ resource: "'unsafe-inline'", kind: 'attribute' },
|
|
77
|
+
]
|
|
78
|
+
: uniqueCspSources(["'self'", ...styleSources]),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function uniqueCspSources(sources) {
|
|
83
|
+
return [...new Set(sources)];
|
|
84
|
+
}
|
|
85
|
+
function cspDirective(name, sources) {
|
|
86
|
+
return `${name} ${[...new Set(sources)].join(' ')}`;
|
|
87
|
+
}
|
|
88
|
+
function exactCspSources(sources) {
|
|
89
|
+
return [...new Set(sources ?? [])].map((source) => {
|
|
90
|
+
const normalized = source.trim();
|
|
91
|
+
if (normalized === "'self'")
|
|
92
|
+
return normalized;
|
|
93
|
+
if (normalized === '' ||
|
|
94
|
+
normalized.includes('*') ||
|
|
95
|
+
normalized === 'https:' ||
|
|
96
|
+
normalized === 'http:' ||
|
|
97
|
+
normalized.startsWith("'unsafe-")) {
|
|
98
|
+
throw new Error(`Astro CSP sources must be exact and cannot use ${source}`);
|
|
99
|
+
}
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = new URL(normalized);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error(`Astro CSP sources must be exact HTTPS/WSS origins: ${source}`);
|
|
106
|
+
}
|
|
107
|
+
if ((parsed.protocol !== 'https:' && parsed.protocol !== 'wss:') ||
|
|
108
|
+
parsed.username !== '' ||
|
|
109
|
+
parsed.password !== '' ||
|
|
110
|
+
parsed.search !== '' ||
|
|
111
|
+
parsed.hash !== '' ||
|
|
112
|
+
(parsed.pathname !== '' && parsed.pathname !== '/')) {
|
|
113
|
+
throw new Error(`Astro CSP sources must be exact HTTPS/WSS origins: ${source}`);
|
|
114
|
+
}
|
|
115
|
+
return parsed.origin;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
32
118
|
export function buildContentSecurityPolicy(input) {
|
|
33
119
|
const options = typeof input === 'string' ? { nonce: input } : (input ?? {});
|
|
34
120
|
const scriptSources = ["'self'"];
|
package/dist/policy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
|
package/dist/runner.js
CHANGED
|
@@ -75,7 +75,8 @@ function checkSecurityHeaders(fixture, html) {
|
|
|
75
75
|
const headerEntries = collectHeaders(fixture.response.headers);
|
|
76
76
|
const headers = normalizeHeaders(fixture.response.headers);
|
|
77
77
|
const findings = [];
|
|
78
|
-
const
|
|
78
|
+
const requiredSecurityHeaders = REQUIRED_SECURITY_HEADERS.filter((name) => name !== 'x-frame-options' || fixture.expected.framePolicy.legacyHeader !== 'omit');
|
|
79
|
+
const missingSecurityHeaders = requiredSecurityHeaders.filter((name) => {
|
|
79
80
|
const values = headerEntries[name] ?? [];
|
|
80
81
|
return !values.some((value) => value.trim().length > 0);
|
|
81
82
|
});
|
|
@@ -116,7 +117,30 @@ function checkSecurityHeaders(fixture, html) {
|
|
|
116
117
|
}
|
|
117
118
|
const csp = headers['content-security-policy'];
|
|
118
119
|
if (csp)
|
|
119
|
-
findings.push(...validateContentSecurityPolicy(csp, html));
|
|
120
|
+
findings.push(...validateContentSecurityPolicy(csp, html, fixture));
|
|
121
|
+
const xFrameOptions = headers['x-frame-options'];
|
|
122
|
+
const expectedLegacyFrameHeader = fixture.expected.framePolicy.legacyHeader;
|
|
123
|
+
if (expectedLegacyFrameHeader === 'omit' && xFrameOptions) {
|
|
124
|
+
findings.push({
|
|
125
|
+
code: 'security.frame_legacy_conflict',
|
|
126
|
+
severity: 'error',
|
|
127
|
+
message: 'X-Frame-Options conflicts with the declared embeddable frame-ancestors policy.',
|
|
128
|
+
path: 'response.headers.x-frame-options',
|
|
129
|
+
evidence: xFrameOptions,
|
|
130
|
+
remediation: 'Omit the legacy header only on the explicitly declared embeddable route and retain the exact CSP frame-ancestors directive.',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else if (expectedLegacyFrameHeader !== 'omit' &&
|
|
134
|
+
xFrameOptions &&
|
|
135
|
+
xFrameOptions.toUpperCase() !== expectedLegacyFrameHeader) {
|
|
136
|
+
findings.push({
|
|
137
|
+
code: 'security.header.invalid',
|
|
138
|
+
severity: 'error',
|
|
139
|
+
message: `X-Frame-Options must be ${expectedLegacyFrameHeader} for this response class.`,
|
|
140
|
+
path: 'response.headers.x-frame-options',
|
|
141
|
+
evidence: xFrameOptions,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
120
144
|
const coop = headers['cross-origin-opener-policy'];
|
|
121
145
|
if (coop && !VALID_COOP_VALUES.has(coop.toLowerCase())) {
|
|
122
146
|
findings.push({
|
|
@@ -310,10 +334,30 @@ function checkSecurityReceipts(fixture) {
|
|
|
310
334
|
}
|
|
311
335
|
return receipt('security-receipts', findings);
|
|
312
336
|
}
|
|
313
|
-
function validateContentSecurityPolicy(csp, html) {
|
|
337
|
+
function validateContentSecurityPolicy(csp, html, fixture) {
|
|
314
338
|
const findings = [];
|
|
315
339
|
const { directives, findings: parseFindings } = parseContentSecurityPolicy(csp);
|
|
316
340
|
findings.push(...parseFindings);
|
|
341
|
+
const frameAncestors = directives.get('frame-ancestors');
|
|
342
|
+
const expectedFrameAncestors = fixture.expected.framePolicy.ancestors;
|
|
343
|
+
if (!frameAncestors) {
|
|
344
|
+
findings.push({
|
|
345
|
+
code: 'security.csp.frame_ancestors_missing',
|
|
346
|
+
severity: 'error',
|
|
347
|
+
message: 'Content-Security-Policy must declare frame-ancestors.',
|
|
348
|
+
path: 'response.headers.content-security-policy.frame-ancestors',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
else if (normalizeCspSources(frameAncestors.sources) !== normalizeCspSources(expectedFrameAncestors)) {
|
|
352
|
+
findings.push({
|
|
353
|
+
code: 'security.csp.frame_ancestors_mismatch',
|
|
354
|
+
severity: 'error',
|
|
355
|
+
message: 'Content-Security-Policy frame-ancestors does not match the declared response class.',
|
|
356
|
+
path: 'response.headers.content-security-policy.frame-ancestors',
|
|
357
|
+
evidence: frameAncestors.raw,
|
|
358
|
+
remediation: 'Emit only the exact declared frame ancestors and keep embeddable routes explicitly bounded.',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
317
361
|
const defaultSources = lowerSources(directives.get('default-src')?.sources ?? []);
|
|
318
362
|
if (defaultSources.includes('*')) {
|
|
319
363
|
findings.push({
|
|
@@ -345,6 +389,32 @@ function validateContentSecurityPolicy(csp, html) {
|
|
|
345
389
|
if (!directive)
|
|
346
390
|
continue;
|
|
347
391
|
if (lowerSources(directive.sources).includes("'unsafe-inline'")) {
|
|
392
|
+
if (directiveName === 'style-src-attr') {
|
|
393
|
+
const inventory = fixture.securityReceipts.inlineStyleAttributes;
|
|
394
|
+
if (!inventory) {
|
|
395
|
+
findings.push({
|
|
396
|
+
code: 'security.csp.style_attr_inline_inventory_missing',
|
|
397
|
+
severity: 'error',
|
|
398
|
+
message: "style-src-attr 'unsafe-inline' requires a commit-bound inline-style-attribute inventory receipt.",
|
|
399
|
+
path: 'securityReceipts.inlineStyleAttributes',
|
|
400
|
+
evidence: directive.raw,
|
|
401
|
+
remediation: 'Remove the allowance or attach the exact consumer commit SHA, positive occurrence count, and scan evidence.',
|
|
402
|
+
});
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (!fixture.buildEvidence?.commitSha ||
|
|
406
|
+
fixture.buildEvidence.commitSha !== inventory.commitSha) {
|
|
407
|
+
findings.push({
|
|
408
|
+
code: 'security.csp.style_attr_inline_inventory_stale',
|
|
409
|
+
severity: 'error',
|
|
410
|
+
message: 'The inline-style-attribute inventory must match the exact commit in build evidence.',
|
|
411
|
+
path: 'securityReceipts.inlineStyleAttributes.commitSha',
|
|
412
|
+
evidence: `inventory=${inventory.commitSha}; build=${fixture.buildEvidence?.commitSha ?? 'missing'}`,
|
|
413
|
+
remediation: 'Rescan the exact build commit before retaining the style attribute allowance.',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
348
418
|
findings.push({
|
|
349
419
|
code: 'security.csp.unsafe_inline',
|
|
350
420
|
severity: 'error',
|
|
@@ -437,7 +507,7 @@ function inlineExecutableScriptAllowed(script, sources) {
|
|
|
437
507
|
return sources.some((source) => hashSourceMatchesInlineScript(source, script.content));
|
|
438
508
|
}
|
|
439
509
|
function hashSourceMatchesInlineScript(source, content) {
|
|
440
|
-
const match = /^'(sha256|sha384|sha512)-([^']+)'
|
|
510
|
+
const match = /^'(sha256|sha384|sha512)-([^']+)'$/.exec(source);
|
|
441
511
|
if (!match)
|
|
442
512
|
return false;
|
|
443
513
|
const algorithm = match[1]?.toLowerCase();
|