@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/src/schema.ts
CHANGED
|
@@ -19,6 +19,7 @@ export const REQUIRED_NEGATIVE_FIXTURE_CODES = {
|
|
|
19
19
|
'csp-unsafe-eval': 'security.csp.unsafe_eval',
|
|
20
20
|
'csp-inline-executable-without-nonce-or-hash':
|
|
21
21
|
'security.csp.inline_executable_without_nonce_or_hash',
|
|
22
|
+
'csp-style-attr-inline-without-inventory': 'security.csp.style_attr_inline_inventory_missing',
|
|
22
23
|
'conflicting-duplicate-security-header': 'security.header.conflicting_values',
|
|
23
24
|
'missing-functional-probe-receipt': 'security.functional_probe.missing',
|
|
24
25
|
'missing-csp-report-receipt': 'security.csp_report.missing',
|
|
@@ -98,10 +99,20 @@ export const cspReportReceiptSchema = z.object({
|
|
|
98
99
|
capabilityBoundary: z.string().min(1).optional(),
|
|
99
100
|
})
|
|
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
|
+
|
|
101
111
|
export const securityReceiptsSchema = z
|
|
102
112
|
.object({
|
|
103
113
|
functionalProbes: z.array(securityFunctionalProbeReceiptSchema).default([]),
|
|
104
114
|
cspReports: z.array(cspReportReceiptSchema).default([]),
|
|
115
|
+
inlineStyleAttributes: inlineStyleAttributeInventorySchema.optional(),
|
|
105
116
|
})
|
|
106
117
|
.default({ functionalProbes: [], cspReports: [] })
|
|
107
118
|
|
|
@@ -114,10 +125,81 @@ export const buildEvidenceSchema = z.object({
|
|
|
114
125
|
ssrHeaderPolicy: z.boolean().default(false),
|
|
115
126
|
})
|
|
116
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
|
+
|
|
117
195
|
export const conformanceExpectedSchema = z.object({
|
|
118
196
|
canonicalUrl: z.string().url().optional(),
|
|
119
197
|
schemaTypes: z.array(z.string().min(1)).default([]),
|
|
120
198
|
discoverEligible: z.boolean().default(true),
|
|
199
|
+
framePolicy: framePolicySchema.default({
|
|
200
|
+
ancestors: ["'none'"],
|
|
201
|
+
legacyHeader: 'DENY',
|
|
202
|
+
}),
|
|
121
203
|
})
|
|
122
204
|
|
|
123
205
|
export const conformanceFixtureSchema = z.object({
|
|
@@ -190,6 +272,7 @@ export type PublicationSurfaceEvidence = z.infer<typeof publicationSurfaceSchema
|
|
|
190
272
|
export type CacheVariant = z.infer<typeof cacheVariantSchema>
|
|
191
273
|
export type SecurityFunctionalProbeReceipt = z.infer<typeof securityFunctionalProbeReceiptSchema>
|
|
192
274
|
export type CspReportReceipt = z.infer<typeof cspReportReceiptSchema>
|
|
275
|
+
export type InlineStyleAttributeInventory = z.infer<typeof inlineStyleAttributeInventorySchema>
|
|
193
276
|
export type SecurityReceipts = z.infer<typeof securityReceiptsSchema>
|
|
194
277
|
export type BuildEvidence = z.infer<typeof buildEvidenceSchema>
|
|
195
278
|
export type ConformanceFixtureInput = z.input<typeof conformanceFixtureSchema>
|