@growth-labs/conformance 0.1.0 → 0.3.0
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 +46 -0
- package/README.md +113 -9
- 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 +54 -3
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +134 -10
- package/dist/policy.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +582 -15
- package/dist/runner.js.map +1 -1
- package/dist/schema.d.ts +829 -12
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +117 -1
- package/dist/schema.js.map +1 -1
- package/fixtures/negative/required-negative-fixtures.json +142 -0
- package/fixtures/valid/public-article.json +56 -2
- package/package.json +7 -1
- package/src/index.ts +19 -0
- package/src/policy.ts +203 -12
- package/src/runner.ts +673 -14
- package/src/schema.ts +132 -1
package/src/schema.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
-
export const EVIDENCE_SCHEMA_VERSION = 'growth-labs.conformance.
|
|
3
|
+
export const EVIDENCE_SCHEMA_VERSION = 'growth-labs.conformance.v2'
|
|
4
|
+
export const REQUIRED_FUNCTIONAL_PROBE_NAMES = [
|
|
5
|
+
'oauth',
|
|
6
|
+
'media',
|
|
7
|
+
'images',
|
|
8
|
+
'analytics',
|
|
9
|
+
'embeds',
|
|
10
|
+
'error-pages',
|
|
11
|
+
] as const
|
|
4
12
|
|
|
5
13
|
export const REQUIRED_NEGATIVE_FIXTURE_CODES = {
|
|
6
14
|
'csp-unsafe-inline': 'security.csp.unsafe_inline',
|
|
15
|
+
'missing-coop': 'security.header.missing',
|
|
16
|
+
'missing-corp': 'security.header.missing',
|
|
17
|
+
'hsts-below-one-year': 'security.hsts.max_age_too_low',
|
|
18
|
+
'csp-wildcard-default-src': 'security.csp.default_src_wildcard',
|
|
19
|
+
'csp-unsafe-eval': 'security.csp.unsafe_eval',
|
|
20
|
+
'csp-inline-executable-without-nonce-or-hash':
|
|
21
|
+
'security.csp.inline_executable_without_nonce_or_hash',
|
|
22
|
+
'csp-style-attr-inline-without-inventory': 'security.csp.style_attr_inline_inventory_missing',
|
|
23
|
+
'conflicting-duplicate-security-header': 'security.header.conflicting_values',
|
|
24
|
+
'missing-functional-probe-receipt': 'security.functional_probe.missing',
|
|
25
|
+
'missing-csp-report-receipt': 'security.csp_report.missing',
|
|
26
|
+
'failed-csp-report-receipt': 'security.csp_report.failed',
|
|
7
27
|
'headers-static-only-while-ssr-missing': 'security.headers.static_only_for_ssr',
|
|
8
28
|
'undeclared-na': 'publication.na.undeclared',
|
|
9
29
|
'stringified-is-accessible-for-free': 'structured_data.is_accessible_for_free.stringified',
|
|
@@ -62,6 +82,40 @@ export const cacheVariantSchema = z.object({
|
|
|
62
82
|
headers: headerMapSchema.default({}),
|
|
63
83
|
})
|
|
64
84
|
|
|
85
|
+
export const securityFunctionalProbeReceiptSchema = z.object({
|
|
86
|
+
name: z.string().min(1),
|
|
87
|
+
status: checkStatusSchema,
|
|
88
|
+
checkedAt: z.string().min(1).optional(),
|
|
89
|
+
evidence: z.string().min(1).optional(),
|
|
90
|
+
capabilityBoundary: z.string().min(1).optional(),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
export const cspReportReceiptSchema = z.object({
|
|
94
|
+
name: z.string().min(1),
|
|
95
|
+
status: checkStatusSchema,
|
|
96
|
+
checkedAt: z.string().min(1).optional(),
|
|
97
|
+
evidence: z.string().min(1).optional(),
|
|
98
|
+
endpoint: z.string().min(1).optional(),
|
|
99
|
+
capabilityBoundary: z.string().min(1).optional(),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
export const inlineStyleAttributeInventorySchema = z.object({
|
|
103
|
+
commitSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
104
|
+
occurrenceCount: z.number().int().positive(),
|
|
105
|
+
evidence: z.object({
|
|
106
|
+
tool: z.string().regex(/^[a-z0-9][a-z0-9:@/._-]*$/i),
|
|
107
|
+
outputHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
108
|
+
}),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
export const securityReceiptsSchema = z
|
|
112
|
+
.object({
|
|
113
|
+
functionalProbes: z.array(securityFunctionalProbeReceiptSchema).default([]),
|
|
114
|
+
cspReports: z.array(cspReportReceiptSchema).default([]),
|
|
115
|
+
inlineStyleAttributes: inlineStyleAttributeInventorySchema.optional(),
|
|
116
|
+
})
|
|
117
|
+
.default({ functionalProbes: [], cspReports: [] })
|
|
118
|
+
|
|
65
119
|
export const buildEvidenceSchema = z.object({
|
|
66
120
|
commitSha: z.string().min(7).optional(),
|
|
67
121
|
artifactId: z.string().min(1).optional(),
|
|
@@ -71,10 +125,81 @@ export const buildEvidenceSchema = z.object({
|
|
|
71
125
|
ssrHeaderPolicy: z.boolean().default(false),
|
|
72
126
|
})
|
|
73
127
|
|
|
128
|
+
const frameAncestorSourceSchema = z.string().superRefine((source, context) => {
|
|
129
|
+
if (source === "'none'" || source === "'self'" || source === 'https:') return
|
|
130
|
+
let parsed: URL
|
|
131
|
+
try {
|
|
132
|
+
parsed = new URL(source)
|
|
133
|
+
} catch {
|
|
134
|
+
context.addIssue({
|
|
135
|
+
code: z.ZodIssueCode.custom,
|
|
136
|
+
message: "Expected 'none', 'self', https:, or an exact HTTPS origin.",
|
|
137
|
+
})
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
parsed.protocol !== 'https:' ||
|
|
142
|
+
parsed.username !== '' ||
|
|
143
|
+
parsed.password !== '' ||
|
|
144
|
+
parsed.search !== '' ||
|
|
145
|
+
parsed.hash !== '' ||
|
|
146
|
+
source !== parsed.origin
|
|
147
|
+
) {
|
|
148
|
+
context.addIssue({
|
|
149
|
+
code: z.ZodIssueCode.custom,
|
|
150
|
+
message: "Expected 'none', 'self', https:, or an exact HTTPS origin.",
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const framePolicySchema = z
|
|
156
|
+
.object({
|
|
157
|
+
ancestors: z
|
|
158
|
+
.array(frameAncestorSourceSchema)
|
|
159
|
+
.min(1)
|
|
160
|
+
.refine((sources) => new Set(sources).size === sources.length, {
|
|
161
|
+
message: 'Frame ancestor sources must be unique.',
|
|
162
|
+
}),
|
|
163
|
+
legacyHeader: z.enum(['DENY', 'SAMEORIGIN', 'omit']),
|
|
164
|
+
})
|
|
165
|
+
.superRefine((policy, context) => {
|
|
166
|
+
if (policy.ancestors.includes("'none'") && policy.ancestors.length !== 1) {
|
|
167
|
+
context.addIssue({
|
|
168
|
+
code: z.ZodIssueCode.custom,
|
|
169
|
+
path: ['ancestors'],
|
|
170
|
+
message: "The 'none' frame ancestor cannot be combined with other sources.",
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
policy.legacyHeader === 'DENY' &&
|
|
175
|
+
(policy.ancestors.length !== 1 || policy.ancestors[0] !== "'none'")
|
|
176
|
+
) {
|
|
177
|
+
context.addIssue({
|
|
178
|
+
code: z.ZodIssueCode.custom,
|
|
179
|
+
path: ['legacyHeader'],
|
|
180
|
+
message: "DENY is compatible only with frame-ancestors 'none'.",
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
if (
|
|
184
|
+
policy.legacyHeader === 'SAMEORIGIN' &&
|
|
185
|
+
(policy.ancestors.length !== 1 || policy.ancestors[0] !== "'self'")
|
|
186
|
+
) {
|
|
187
|
+
context.addIssue({
|
|
188
|
+
code: z.ZodIssueCode.custom,
|
|
189
|
+
path: ['legacyHeader'],
|
|
190
|
+
message: "SAMEORIGIN is compatible only with frame-ancestors 'self'.",
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
74
195
|
export const conformanceExpectedSchema = z.object({
|
|
75
196
|
canonicalUrl: z.string().url().optional(),
|
|
76
197
|
schemaTypes: z.array(z.string().min(1)).default([]),
|
|
77
198
|
discoverEligible: z.boolean().default(true),
|
|
199
|
+
framePolicy: framePolicySchema.default({
|
|
200
|
+
ancestors: ["'none'"],
|
|
201
|
+
legacyHeader: 'DENY',
|
|
202
|
+
}),
|
|
78
203
|
})
|
|
79
204
|
|
|
80
205
|
export const conformanceFixtureSchema = z.object({
|
|
@@ -88,6 +213,7 @@ export const conformanceFixtureSchema = z.object({
|
|
|
88
213
|
expected: conformanceExpectedSchema.default({}),
|
|
89
214
|
publicationSurfaces: z.array(publicationSurfaceSchema).default([]),
|
|
90
215
|
cacheVariants: z.array(cacheVariantSchema).default([]),
|
|
216
|
+
securityReceipts: securityReceiptsSchema,
|
|
91
217
|
buildEvidence: buildEvidenceSchema.optional(),
|
|
92
218
|
expectedFailingCode: z.string().min(1).optional(),
|
|
93
219
|
})
|
|
@@ -125,6 +251,7 @@ export const siteEvidenceBundleSchema = z.object({
|
|
|
125
251
|
summary: evidenceSummarySchema,
|
|
126
252
|
checks: z.array(conformanceCheckReceiptSchema),
|
|
127
253
|
findings: z.array(conformanceFindingSchema),
|
|
254
|
+
securityReceipts: securityReceiptsSchema,
|
|
128
255
|
})
|
|
129
256
|
|
|
130
257
|
export const fleetEvidenceBundleSchema = z.object({
|
|
@@ -143,6 +270,10 @@ export type HeaderMap = z.infer<typeof headerMapSchema>
|
|
|
143
270
|
export type SurfaceResponse = z.infer<typeof surfaceResponseSchema>
|
|
144
271
|
export type PublicationSurfaceEvidence = z.infer<typeof publicationSurfaceSchema>
|
|
145
272
|
export type CacheVariant = z.infer<typeof cacheVariantSchema>
|
|
273
|
+
export type SecurityFunctionalProbeReceipt = z.infer<typeof securityFunctionalProbeReceiptSchema>
|
|
274
|
+
export type CspReportReceipt = z.infer<typeof cspReportReceiptSchema>
|
|
275
|
+
export type InlineStyleAttributeInventory = z.infer<typeof inlineStyleAttributeInventorySchema>
|
|
276
|
+
export type SecurityReceipts = z.infer<typeof securityReceiptsSchema>
|
|
146
277
|
export type BuildEvidence = z.infer<typeof buildEvidenceSchema>
|
|
147
278
|
export type ConformanceFixtureInput = z.input<typeof conformanceFixtureSchema>
|
|
148
279
|
export type ConformanceFixture = z.infer<typeof conformanceFixtureSchema>
|