@pikku/inspector 0.12.75 → 0.12.81
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 +330 -0
- package/dist/add/add-analytics.d.ts +9 -0
- package/dist/add/add-analytics.js +178 -0
- package/dist/add/add-auth.js +0 -54
- package/dist/add/add-feature-flag.d.ts +2 -0
- package/dist/add/add-feature-flag.js +162 -0
- package/dist/add/add-wire-addon.js +1 -0
- package/dist/add/add-wire-remote-addon.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/inspector.js +4 -0
- package/dist/surface/collect-surface.d.ts +58 -0
- package/dist/surface/collect-surface.js +613 -0
- package/dist/types.d.ts +20 -11
- package/dist/utils/filter-inspector-state.js +14 -0
- package/dist/utils/post-process.js +15 -0
- package/dist/utils/read-module-specifiers.d.ts +8 -0
- package/dist/utils/read-module-specifiers.js +23 -0
- package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
- package/dist/utils/read-tsconfig-out-dir.js +12 -0
- package/dist/utils/resolve-deploy-target.d.ts +7 -0
- package/dist/utils/resolve-deploy-target.js +15 -7
- package/dist/utils/serialize-inspector-state.d.ts +5 -0
- package/dist/utils/serialize-inspector-state.js +8 -0
- package/dist/visit.js +4 -0
- package/package.json +16 -6
- package/run-tests.sh +21 -6
- package/src/add/add-addon-bans.ts +0 -85
- package/src/add/add-agent-scorer.test.ts +0 -197
- package/src/add/add-agent-scorer.ts +0 -83
- package/src/add/add-agent.test.ts +0 -87
- package/src/add/add-agent.ts +0 -638
- package/src/add/add-approval-description.ts +0 -76
- package/src/add/add-auth.test.ts +0 -791
- package/src/add/add-auth.ts +0 -424
- package/src/add/add-channel.ts +0 -778
- package/src/add/add-cli-renderers.test.ts +0 -74
- package/src/add/add-cli.ts +0 -1006
- package/src/add/add-credential.ts +0 -186
- package/src/add/add-feature.test.ts +0 -306
- package/src/add/add-feature.ts +0 -253
- package/src/add/add-file-extends-core-type.ts +0 -66
- package/src/add/add-file-with-config.ts +0 -45
- package/src/add/add-file-with-factory.test.ts +0 -134
- package/src/add/add-file-with-factory.ts +0 -288
- package/src/add/add-functions.test.ts +0 -743
- package/src/add/add-functions.ts +0 -1548
- package/src/add/add-gateway.ts +0 -143
- package/src/add/add-http-route-ref.test.ts +0 -149
- package/src/add/add-http-route.ts +0 -530
- package/src/add/add-http-routes.ts +0 -471
- package/src/add/add-keyed-wiring.ts +0 -180
- package/src/add/add-mcp-prompt.ts +0 -157
- package/src/add/add-mcp-resource.ts +0 -175
- package/src/add/add-middleware.test.ts +0 -196
- package/src/add/add-middleware.ts +0 -879
- package/src/add/add-permission-auth.test.ts +0 -59
- package/src/add/add-permission.ts +0 -311
- package/src/add/add-personas.test.ts +0 -406
- package/src/add/add-personas.ts +0 -287
- package/src/add/add-queue-worker.ts +0 -117
- package/src/add/add-rpc-invocations.test.ts +0 -287
- package/src/add/add-rpc-invocations.ts +0 -221
- package/src/add/add-scenario-step.test.ts +0 -509
- package/src/add/add-schedule.ts +0 -120
- package/src/add/add-scope.test.ts +0 -419
- package/src/add/add-scope.ts +0 -221
- package/src/add/add-secret.ts +0 -10
- package/src/add/add-subject-scenario-steps.test.ts +0 -161
- package/src/add/add-system-role.test.ts +0 -297
- package/src/add/add-system-role.ts +0 -162
- package/src/add/add-trigger.ts +0 -165
- package/src/add/add-variable.ts +0 -10
- package/src/add/add-wire-addon.test.ts +0 -185
- package/src/add/add-wire-addon.ts +0 -146
- package/src/add/add-wire-remote-addon.ts +0 -80
- package/src/add/add-workflow-edge-cases.test.ts +0 -201
- package/src/add/add-workflow-fanout.test.ts +0 -166
- package/src/add/add-workflow-graph-foreach.test.ts +0 -174
- package/src/add/add-workflow-graph-input.test.ts +0 -94
- package/src/add/add-workflow-graph-notes.test.ts +0 -74
- package/src/add/add-workflow-graph.ts +0 -644
- package/src/add/add-workflow-on-error.test.ts +0 -118
- package/src/add/add-workflow-return-switch.test.ts +0 -121
- package/src/add/add-workflow-silent-drops.test.ts +0 -261
- package/src/add/add-workflow-sleep-expression.test.ts +0 -96
- package/src/add/add-workflow.test.ts +0 -155
- package/src/add/add-workflow.ts +0 -482
- package/src/add/addon-bans.test.ts +0 -121
- package/src/add/addon-contracts.test.ts +0 -221
- package/src/add/allow-escape-hatches.test.ts +0 -144
- package/src/add/audit-meta.test.ts +0 -75
- package/src/add/classified-sink-check.test.ts +0 -347
- package/src/add/config-value-metadata.test.ts +0 -263
- package/src/add/dynamic-import-check.test.ts +0 -119
- package/src/add/expect-eventually-scenario-only.test.ts +0 -108
- package/src/add/function-body-span.test.ts +0 -96
- package/src/add/inline-wiring-function.test.ts +0 -104
- package/src/add/pii-check.test.ts +0 -258
- package/src/add/rpc-type-cast.test.ts +0 -123
- package/src/add/scenario-instrumentation.test.ts +0 -89
- package/src/add/scenario-instrumentation.ts +0 -42
- package/src/add/scenario-marker.test.ts +0 -89
- package/src/add/scenario-not-extractable.test.ts +0 -159
- package/src/add/scenario-services.test.ts +0 -103
- package/src/add/scenario-skip.test.ts +0 -83
- package/src/add/validate-workflow-graph-addons.test.ts +0 -102
- package/src/add/wire-name-literal.test.ts +0 -117
- package/src/error-codes-emitted.test.ts +0 -76
- package/src/error-codes.ts +0 -145
- package/src/index.ts +0 -66
- package/src/inspector.ts +0 -575
- package/src/schema-generator.ts +0 -1
- package/src/source-file-cache.ts +0 -73
- package/src/tsconfig-paths.test.ts +0 -90
- package/src/types-map.ts +0 -141
- package/src/types.ts +0 -775
- package/src/utils/annotate-http-route-auth.test.ts +0 -161
- package/src/utils/annotate-http-route-auth.ts +0 -53
- package/src/utils/check-classified-sinks.ts +0 -215
- package/src/utils/check-pii-output.ts +0 -120
- package/src/utils/compute-diagnostics.test.ts +0 -106
- package/src/utils/compute-required-schemas.ts +0 -48
- package/src/utils/contract-hashes.test.ts +0 -553
- package/src/utils/contract-hashes.ts +0 -387
- package/src/utils/custom-types-generator.test.ts +0 -124
- package/src/utils/custom-types-generator.ts +0 -150
- package/src/utils/detect-schema-vendor.test.ts +0 -123
- package/src/utils/detect-schema-vendor.ts +0 -120
- package/src/utils/does-type-extend-core-type.ts +0 -53
- package/src/utils/ensure-function-metadata.ts +0 -385
- package/src/utils/extract-function-name.ts +0 -612
- package/src/utils/extract-function-node.ts +0 -70
- package/src/utils/extract-node-value.test.ts +0 -117
- package/src/utils/extract-node-value.ts +0 -172
- package/src/utils/extract-secret-usage.test.ts +0 -81
- package/src/utils/extract-secret-usage.ts +0 -73
- package/src/utils/extract-services.ts +0 -70
- package/src/utils/extract-surface-usage.test.ts +0 -147
- package/src/utils/extract-surface-usage.ts +0 -121
- package/src/utils/filter-inspector-state.test.ts +0 -2232
- package/src/utils/filter-inspector-state.ts +0 -1298
- package/src/utils/filter-utils.test.ts +0 -843
- package/src/utils/filter-utils.ts +0 -161
- package/src/utils/find-root-dir.ts +0 -68
- package/src/utils/find-zod-transform.test.ts +0 -70
- package/src/utils/find-zod-transform.ts +0 -103
- package/src/utils/get-exported-variable-name.ts +0 -48
- package/src/utils/get-files-and-methods.ts +0 -157
- package/src/utils/get-property-value.test.ts +0 -141
- package/src/utils/get-property-value.ts +0 -331
- package/src/utils/hash.ts +0 -26
- package/src/utils/literal-properties.ts +0 -97
- package/src/utils/load-addon-functions-meta.test.ts +0 -157
- package/src/utils/load-addon-functions-meta.ts +0 -508
- package/src/utils/load-addon-scopes.test.ts +0 -138
- package/src/utils/middleware.test.ts +0 -204
- package/src/utils/middleware.ts +0 -303
- package/src/utils/nested-project-filter.test.ts +0 -53
- package/src/utils/nested-project-filter.ts +0 -28
- package/src/utils/permissions-shorthand.test.ts +0 -79
- package/src/utils/permissions.test.ts +0 -125
- package/src/utils/permissions.ts +0 -129
- package/src/utils/post-process.test.ts +0 -1094
- package/src/utils/post-process.ts +0 -1604
- package/src/utils/resolve-addon-package.ts +0 -106
- package/src/utils/resolve-core-type.test.ts +0 -142
- package/src/utils/resolve-core-type.ts +0 -35
- package/src/utils/resolve-deploy-target.test.ts +0 -135
- package/src/utils/resolve-deploy-target.ts +0 -65
- package/src/utils/resolve-function-meta.ts +0 -25
- package/src/utils/resolve-function-types.ts +0 -42
- package/src/utils/resolve-identifier.ts +0 -46
- package/src/utils/resolve-ref-contract.ts +0 -71
- package/src/utils/resolve-versions.test.ts +0 -499
- package/src/utils/resolve-versions.ts +0 -228
- package/src/utils/schema-cache.test.ts +0 -124
- package/src/utils/schema-generator.test.ts +0 -272
- package/src/utils/schema-generator.ts +0 -780
- package/src/utils/secret-alias-services.test.ts +0 -146
- package/src/utils/secret-alias-services.ts +0 -58
- package/src/utils/serialize-inspector-state.ts +0 -742
- package/src/utils/serialize-mcp-json.ts +0 -150
- package/src/utils/serialize-middleware-groups-meta.ts +0 -36
- package/src/utils/serialize-openapi-json.ts +0 -277
- package/src/utils/serialize-permissions-groups-meta.ts +0 -7
- package/src/utils/single-declaration.ts +0 -57
- package/src/utils/type-utils.ts +0 -145
- package/src/utils/validate-auth-sessionless.ts +0 -30
- package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
- package/src/utils/validate-exposed-functions-gated.ts +0 -103
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
- package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
- package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
- package/src/utils/workflow/derive-workflow-plan.ts +0 -117
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
- package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
- package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
- package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
- package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
- package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
- package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
- package/src/utils/workflow/dsl/index.ts +0 -11
- package/src/utils/workflow/dsl/patterns.ts +0 -440
- package/src/utils/workflow/dsl/validation.ts +0 -203
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
- package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
- package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
- package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
- package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
- package/src/utils/workflow/graph/index.ts +0 -28
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
- package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
- package/src/visit.ts +0 -181
- package/tsconfig.json +0 -19
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from 'assert'
|
|
2
|
-
import { describe, test } from 'node:test'
|
|
3
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
4
|
-
import { tmpdir } from 'node:os'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
-
import { inspect } from '../inspector.js'
|
|
7
|
-
import { ErrorCode } from '../error-codes.js'
|
|
8
|
-
import type { InspectorLogger } from '../types.js'
|
|
9
|
-
|
|
10
|
-
function makeLogger() {
|
|
11
|
-
const diagnostics: Array<{ code: ErrorCode; message: string }> = []
|
|
12
|
-
const logger: InspectorLogger = {
|
|
13
|
-
debug: () => {},
|
|
14
|
-
info: () => {},
|
|
15
|
-
warn: () => {},
|
|
16
|
-
error: () => {},
|
|
17
|
-
diagnostic: ({ code, message }) => diagnostics.push({ code, message }),
|
|
18
|
-
critical: (code, message) => diagnostics.push({ code, message }),
|
|
19
|
-
hasCriticalErrors: () => diagnostics.length > 0,
|
|
20
|
-
}
|
|
21
|
-
return { logger, diagnostics }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Stands in for @pikku/core, which is not importable from a tmp dir. The shapes
|
|
26
|
-
* are what matters: `Secret<T>` is the optional brand `.reveal()` returns,
|
|
27
|
-
* `Pii<T>` and `Private<T>` are the brands a generated row type carries, and the
|
|
28
|
-
* service interfaces are matched by name, so these must keep the real names.
|
|
29
|
-
*/
|
|
30
|
-
const PRELUDE = `
|
|
31
|
-
type Secret<T> = T & { readonly __classification__?: 'secret' }
|
|
32
|
-
type Pii<T> = T & { readonly __classification__?: 'pii' }
|
|
33
|
-
type Private<T> = T & { readonly __classification__?: 'private' }
|
|
34
|
-
|
|
35
|
-
/** What \`db.selectFrom('user')\` hands back once the schema is generated. */
|
|
36
|
-
interface UserRow {
|
|
37
|
-
userId: string
|
|
38
|
-
email: Pii<string>
|
|
39
|
-
internalNote: Private<string>
|
|
40
|
-
}
|
|
41
|
-
declare function getUser(): Promise<UserRow>
|
|
42
|
-
|
|
43
|
-
declare class SecretValue<T = string> {
|
|
44
|
-
private brand: T
|
|
45
|
-
reveal(): Secret<T>
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface Logger {
|
|
49
|
-
info(messageOrObj: any, ...meta: any[]): void
|
|
50
|
-
error(messageOrObj: any, ...meta: any[]): void
|
|
51
|
-
}
|
|
52
|
-
interface QueueService {
|
|
53
|
-
add<T>(queueName: string, data: T): Promise<string>
|
|
54
|
-
}
|
|
55
|
-
interface EmailService {
|
|
56
|
-
send(input: any): Promise<void>
|
|
57
|
-
}
|
|
58
|
-
interface WebhookService {
|
|
59
|
-
send(input: any): Promise<void>
|
|
60
|
-
}
|
|
61
|
-
interface AuditService {
|
|
62
|
-
audit(event: { type: string; input?: unknown }): Promise<void>
|
|
63
|
-
}
|
|
64
|
-
interface AuditLog {
|
|
65
|
-
write(event: { type: string; input?: unknown }): Promise<void>
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
type Services = {
|
|
69
|
-
logger: Logger
|
|
70
|
-
queueService: QueueService
|
|
71
|
-
email: EmailService
|
|
72
|
-
webhooks: WebhookService
|
|
73
|
-
audit?: AuditService
|
|
74
|
-
auditLog?: AuditLog
|
|
75
|
-
secrets: { getSecret(key: string): Promise<SecretValue<string>> }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
declare const pikkuFunc: <In, Out>(cfg: {
|
|
79
|
-
func: (services: Services, data: In) => Promise<Out>
|
|
80
|
-
}) => unknown
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
async function runInspect(source: string) {
|
|
84
|
-
const tmpDir = await mkdtemp(join(tmpdir(), 'pikku-secret-sink-test-'))
|
|
85
|
-
const file = join(tmpDir, 'funcs.ts')
|
|
86
|
-
await writeFile(file, PRELUDE + source)
|
|
87
|
-
const { logger, diagnostics } = makeLogger()
|
|
88
|
-
try {
|
|
89
|
-
await inspect(logger, [file], {
|
|
90
|
-
rootDir: tmpDir,
|
|
91
|
-
classificationCheck: true,
|
|
92
|
-
})
|
|
93
|
-
} finally {
|
|
94
|
-
await rm(tmpDir, { recursive: true, force: true })
|
|
95
|
-
}
|
|
96
|
-
return diagnostics
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const sinkErrors = (diagnostics: Array<{ code: ErrorCode; message: string }>) =>
|
|
100
|
-
diagnostics.filter((d) => d.code === ErrorCode.SECRET_REVEALED_INTO_SINK)
|
|
101
|
-
|
|
102
|
-
const piiErrors = (diagnostics: Array<{ code: ErrorCode; message: string }>) =>
|
|
103
|
-
diagnostics.filter((d) => d.code === ErrorCode.PII_INTO_SINK)
|
|
104
|
-
|
|
105
|
-
describe('PKU953 — a revealed secret reaching a sink', () => {
|
|
106
|
-
test('flags a secret logged inline', async () => {
|
|
107
|
-
const diagnostics = await runInspect(`
|
|
108
|
-
export const leak = pikkuFunc<void, void>({
|
|
109
|
-
func: async ({ logger, secrets }) => {
|
|
110
|
-
const token = await secrets.getSecret('API_KEY')
|
|
111
|
-
logger.info(token.reveal())
|
|
112
|
-
},
|
|
113
|
-
})
|
|
114
|
-
`)
|
|
115
|
-
const errors = sinkErrors(diagnostics)
|
|
116
|
-
assert.equal(errors.length, 1)
|
|
117
|
-
assert.match(errors[0]!.message, /logger\.info/)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
test('flags a secret that reaches the sink through a local binding', async () => {
|
|
121
|
-
const diagnostics = await runInspect(`
|
|
122
|
-
export const leak = pikkuFunc<void, void>({
|
|
123
|
-
func: async ({ logger, secrets }) => {
|
|
124
|
-
const token = (await secrets.getSecret('API_KEY')).reveal()
|
|
125
|
-
logger.error('auth failed', { token })
|
|
126
|
-
},
|
|
127
|
-
})
|
|
128
|
-
`)
|
|
129
|
-
const errors = sinkErrors(diagnostics)
|
|
130
|
-
assert.equal(errors.length, 1)
|
|
131
|
-
assert.match(errors[0]!.message, /logger\.error/)
|
|
132
|
-
assert.match(errors[0]!.message, /token/)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
test('flags a secret enqueued, emailed or sent to a webhook', async () => {
|
|
136
|
-
const diagnostics = await runInspect(`
|
|
137
|
-
export const leak = pikkuFunc<void, void>({
|
|
138
|
-
func: async ({ queueService, email, webhooks, secrets }) => {
|
|
139
|
-
const token = (await secrets.getSecret('API_KEY')).reveal()
|
|
140
|
-
await queueService.add('jobs', { token })
|
|
141
|
-
await email.send({ to: 'a@b.c', text: token })
|
|
142
|
-
await webhooks.send({ url: 'https://x', data: { token } })
|
|
143
|
-
},
|
|
144
|
-
})
|
|
145
|
-
`)
|
|
146
|
-
assert.equal(sinkErrors(diagnostics).length, 3)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
test('flags a secret written to an audit, through the optional chain it is reached by', async () => {
|
|
150
|
-
const diagnostics = await runInspect(`
|
|
151
|
-
export const leak = pikkuFunc<void, void>({
|
|
152
|
-
func: async ({ audit, auditLog, secrets }) => {
|
|
153
|
-
const token = (await secrets.getSecret('API_KEY')).reveal()
|
|
154
|
-
await audit?.audit({ type: 'used', input: { token } })
|
|
155
|
-
await auditLog?.write({ type: 'used', input: token })
|
|
156
|
-
},
|
|
157
|
-
})
|
|
158
|
-
`)
|
|
159
|
-
const errors = sinkErrors(diagnostics)
|
|
160
|
-
assert.equal(errors.length, 2)
|
|
161
|
-
assert.match(errors[0]!.message, /audit\.audit/)
|
|
162
|
-
assert.match(errors[1]!.message, /auditLog\.write/)
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
test('flags console, which no type guard covers', async () => {
|
|
166
|
-
const diagnostics = await runInspect(`
|
|
167
|
-
export const leak = pikkuFunc<void, void>({
|
|
168
|
-
func: async ({ secrets }) => {
|
|
169
|
-
const token = await secrets.getSecret('API_KEY')
|
|
170
|
-
console.log(token.reveal())
|
|
171
|
-
},
|
|
172
|
-
})
|
|
173
|
-
`)
|
|
174
|
-
const errors = sinkErrors(diagnostics)
|
|
175
|
-
assert.equal(errors.length, 1)
|
|
176
|
-
assert.match(errors[0]!.message, /console\.log/)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
test('stays quiet when the secret never reaches a sink', async () => {
|
|
180
|
-
const diagnostics = await runInspect(`
|
|
181
|
-
declare function authenticate(token: string): Promise<void>
|
|
182
|
-
|
|
183
|
-
export const fine = pikkuFunc<void, void>({
|
|
184
|
-
func: async ({ logger, secrets }) => {
|
|
185
|
-
const token = (await secrets.getSecret('API_KEY')).reveal()
|
|
186
|
-
await authenticate(token)
|
|
187
|
-
logger.info('authenticated')
|
|
188
|
-
},
|
|
189
|
-
})
|
|
190
|
-
`)
|
|
191
|
-
assert.equal(sinkErrors(diagnostics).length, 0)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
test('stays quiet for ordinary logging', async () => {
|
|
195
|
-
const diagnostics = await runInspect(`
|
|
196
|
-
export const fine = pikkuFunc<{ id: string }, void>({
|
|
197
|
-
func: async ({ logger }, data) => {
|
|
198
|
-
logger.info('processing', { id: data.id, at: new Date() })
|
|
199
|
-
},
|
|
200
|
-
})
|
|
201
|
-
`)
|
|
202
|
-
assert.equal(sinkErrors(diagnostics).length, 0)
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
test('does not run without the classification flag', async () => {
|
|
206
|
-
const tmpDir = await mkdtemp(join(tmpdir(), 'pikku-secret-sink-off-'))
|
|
207
|
-
const file = join(tmpDir, 'funcs.ts')
|
|
208
|
-
await writeFile(
|
|
209
|
-
file,
|
|
210
|
-
PRELUDE +
|
|
211
|
-
`
|
|
212
|
-
export const leak = pikkuFunc<void, void>({
|
|
213
|
-
func: async ({ logger, secrets }) => {
|
|
214
|
-
const token = await secrets.getSecret('API_KEY')
|
|
215
|
-
logger.info(token.reveal())
|
|
216
|
-
},
|
|
217
|
-
})
|
|
218
|
-
`
|
|
219
|
-
)
|
|
220
|
-
const { logger, diagnostics } = makeLogger()
|
|
221
|
-
try {
|
|
222
|
-
await inspect(logger, [file], { rootDir: tmpDir })
|
|
223
|
-
} finally {
|
|
224
|
-
await rm(tmpDir, { recursive: true, force: true })
|
|
225
|
-
}
|
|
226
|
-
assert.equal(sinkErrors(diagnostics).length, 0)
|
|
227
|
-
})
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* PII is not a secret, so the policy is not "never write it down" — it is "not
|
|
232
|
-
* where it leaves your control". A log ships to an aggregator and a webhook
|
|
233
|
-
* posts to somebody else's server; an email is addressed to the data subject,
|
|
234
|
-
* and a queue and an audit stay on the operator's own infrastructure.
|
|
235
|
-
*/
|
|
236
|
-
describe('PKU954 — personal data reaching a sink', () => {
|
|
237
|
-
test('flags PII logged inline and through a local binding', async () => {
|
|
238
|
-
const diagnostics = await runInspect(`
|
|
239
|
-
export const leak = pikkuFunc<void, void>({
|
|
240
|
-
func: async ({ logger }) => {
|
|
241
|
-
const user = await getUser()
|
|
242
|
-
logger.info(user.email)
|
|
243
|
-
const email = user.email
|
|
244
|
-
logger.error('send failed', { email })
|
|
245
|
-
},
|
|
246
|
-
})
|
|
247
|
-
`)
|
|
248
|
-
const errors = piiErrors(diagnostics)
|
|
249
|
-
assert.equal(errors.length, 2)
|
|
250
|
-
assert.match(errors[0]!.message, /logger\.info/)
|
|
251
|
-
assert.match(errors[1]!.message, /logger\.error/)
|
|
252
|
-
assert.equal(sinkErrors(diagnostics).length, 0)
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
test('flags a whole row logged, since the PII rides along inside it', async () => {
|
|
256
|
-
const diagnostics = await runInspect(`
|
|
257
|
-
export const leak = pikkuFunc<void, void>({
|
|
258
|
-
func: async ({ logger }) => {
|
|
259
|
-
const user = await getUser()
|
|
260
|
-
logger.info('loaded', user)
|
|
261
|
-
},
|
|
262
|
-
})
|
|
263
|
-
`)
|
|
264
|
-
assert.equal(piiErrors(diagnostics).length, 1)
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
test('flags PII on console and in a webhook', async () => {
|
|
268
|
-
const diagnostics = await runInspect(`
|
|
269
|
-
export const leak = pikkuFunc<void, void>({
|
|
270
|
-
func: async ({ webhooks }) => {
|
|
271
|
-
const user = await getUser()
|
|
272
|
-
console.log(user.email)
|
|
273
|
-
await webhooks.send({ url: 'https://x', data: { email: user.email } })
|
|
274
|
-
},
|
|
275
|
-
})
|
|
276
|
-
`)
|
|
277
|
-
const errors = piiErrors(diagnostics)
|
|
278
|
-
assert.equal(errors.length, 2)
|
|
279
|
-
assert.match(errors[0]!.message, /console\.log/)
|
|
280
|
-
assert.match(errors[1]!.message, /webhooks\.send/)
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
test('stays quiet for an email to the data subject, an audit and a queue', async () => {
|
|
284
|
-
const diagnostics = await runInspect(`
|
|
285
|
-
export const fine = pikkuFunc<void, void>({
|
|
286
|
-
func: async ({ email, audit, auditLog, queueService }) => {
|
|
287
|
-
const user = await getUser()
|
|
288
|
-
await email.send({ to: user.email, text: 'Hi ' + user.email })
|
|
289
|
-
await audit?.audit({ type: 'viewed', input: { email: user.email } })
|
|
290
|
-
await auditLog?.write({ type: 'viewed', input: user.email })
|
|
291
|
-
await queueService.add('welcome', { email: user.email })
|
|
292
|
-
},
|
|
293
|
-
})
|
|
294
|
-
`)
|
|
295
|
-
assert.equal(piiErrors(diagnostics).length, 0)
|
|
296
|
-
assert.equal(sinkErrors(diagnostics).length, 0)
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
test('stays quiet for private, which is about who may read the row', async () => {
|
|
300
|
-
const diagnostics = await runInspect(`
|
|
301
|
-
export const fine = pikkuFunc<void, void>({
|
|
302
|
-
func: async ({ logger }) => {
|
|
303
|
-
const user = await getUser()
|
|
304
|
-
logger.info('note', { note: user.internalNote, id: user.userId })
|
|
305
|
-
},
|
|
306
|
-
})
|
|
307
|
-
`)
|
|
308
|
-
assert.equal(piiErrors(diagnostics).length, 0)
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
test('stays quiet when an explicit annotation erases the brand', async () => {
|
|
312
|
-
const diagnostics = await runInspect(`
|
|
313
|
-
export const documentedLimit = pikkuFunc<void, void>({
|
|
314
|
-
func: async ({ logger }) => {
|
|
315
|
-
const user = await getUser()
|
|
316
|
-
const email: string = user.email
|
|
317
|
-
logger.info(email)
|
|
318
|
-
},
|
|
319
|
-
})
|
|
320
|
-
`)
|
|
321
|
-
assert.equal(piiErrors(diagnostics).length, 0)
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
test('does not run without the classification flag', async () => {
|
|
325
|
-
const tmpDir = await mkdtemp(join(tmpdir(), 'pikku-pii-sink-off-'))
|
|
326
|
-
const file = join(tmpDir, 'funcs.ts')
|
|
327
|
-
await writeFile(
|
|
328
|
-
file,
|
|
329
|
-
PRELUDE +
|
|
330
|
-
`
|
|
331
|
-
export const leak = pikkuFunc<void, void>({
|
|
332
|
-
func: async ({ logger }) => {
|
|
333
|
-
const user = await getUser()
|
|
334
|
-
logger.info(user.email)
|
|
335
|
-
},
|
|
336
|
-
})
|
|
337
|
-
`
|
|
338
|
-
)
|
|
339
|
-
const { logger, diagnostics } = makeLogger()
|
|
340
|
-
try {
|
|
341
|
-
await inspect(logger, [file], { rootDir: tmpDir })
|
|
342
|
-
} finally {
|
|
343
|
-
await rm(tmpDir, { recursive: true, force: true })
|
|
344
|
-
}
|
|
345
|
-
assert.equal(piiErrors(diagnostics).length, 0)
|
|
346
|
-
})
|
|
347
|
-
})
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from 'assert'
|
|
2
|
-
import { describe, test } from 'node:test'
|
|
3
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
|
-
import { inspect } from '../inspector.js'
|
|
7
|
-
import type { ErrorCode } from '../error-codes.js'
|
|
8
|
-
import type { InspectorLogger } from '../types.js'
|
|
9
|
-
|
|
10
|
-
// Fixtures must sit inside the package so `zod` resolves — the schema-vendor
|
|
11
|
-
// check rejects a schema it cannot trace to a supported library, and the
|
|
12
|
-
// definition is then never recorded at all.
|
|
13
|
-
const fixtureRoot = join(
|
|
14
|
-
fileURLToPath(new URL('.', import.meta.url)),
|
|
15
|
-
'__fixtures'
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
|
|
19
|
-
({
|
|
20
|
-
debug: () => {},
|
|
21
|
-
info: () => {},
|
|
22
|
-
warn: () => {},
|
|
23
|
-
error: () => {},
|
|
24
|
-
diagnostic: ({ code, message }) => {
|
|
25
|
-
criticals.push({ code, message })
|
|
26
|
-
},
|
|
27
|
-
critical: (code: ErrorCode, message: string) => {
|
|
28
|
-
criticals.push({ code, message })
|
|
29
|
-
},
|
|
30
|
-
hasCriticalErrors: () => criticals.length > 0,
|
|
31
|
-
}) satisfies InspectorLogger
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* `docsUrl` is only useful if it survives inspection — a field declared on the
|
|
35
|
-
* wiring but dropped by the inspector is invisible to every consumer
|
|
36
|
-
* downstream, which is exactly how `displayName` went unused.
|
|
37
|
-
*/
|
|
38
|
-
describe('config value metadata (docsUrl)', () => {
|
|
39
|
-
test('carries docsUrl from defineSecret into the definition', async () => {
|
|
40
|
-
const rootDir = await mkdtemp(`${fixtureRoot}-secret-`)
|
|
41
|
-
const file = join(rootDir, 'secret.ts')
|
|
42
|
-
|
|
43
|
-
await writeFile(
|
|
44
|
-
file,
|
|
45
|
-
[
|
|
46
|
-
"import { defineSecret } from '@pikku/core'",
|
|
47
|
-
"import { z } from 'zod'",
|
|
48
|
-
'export const StripeKeySchema = z.string()',
|
|
49
|
-
'defineSecret({',
|
|
50
|
-
" name: 'stripeKey',",
|
|
51
|
-
" displayName: 'Stripe Secret Key',",
|
|
52
|
-
" secretId: 'STRIPE_SECRET_KEY',",
|
|
53
|
-
" docsUrl: 'https://dashboard.stripe.com/apikeys',",
|
|
54
|
-
' schema: StripeKeySchema,',
|
|
55
|
-
'})',
|
|
56
|
-
].join('\n')
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
60
|
-
try {
|
|
61
|
-
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
62
|
-
const def = state.secrets.definitions.find(
|
|
63
|
-
(d) => d.secretId === 'STRIPE_SECRET_KEY'
|
|
64
|
-
)
|
|
65
|
-
assert.ok(def, 'expected the secret definition to be inspected')
|
|
66
|
-
assert.equal(def!.docsUrl, 'https://dashboard.stripe.com/apikeys')
|
|
67
|
-
} finally {
|
|
68
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('leaves docsUrl undefined when not declared', async () => {
|
|
73
|
-
const rootDir = await mkdtemp(`${fixtureRoot}-bare-`)
|
|
74
|
-
const file = join(rootDir, 'secret.ts')
|
|
75
|
-
|
|
76
|
-
await writeFile(
|
|
77
|
-
file,
|
|
78
|
-
[
|
|
79
|
-
"import { defineSecret } from '@pikku/core'",
|
|
80
|
-
"import { z } from 'zod'",
|
|
81
|
-
'export const ApiKeySchema = z.string()',
|
|
82
|
-
'defineSecret({',
|
|
83
|
-
" name: 'apiKey',",
|
|
84
|
-
" displayName: 'API Key',",
|
|
85
|
-
" secretId: 'API_KEY',",
|
|
86
|
-
' schema: ApiKeySchema,',
|
|
87
|
-
'})',
|
|
88
|
-
].join('\n')
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
92
|
-
try {
|
|
93
|
-
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
94
|
-
const def = state.secrets.definitions.find(
|
|
95
|
-
(d) => d.secretId === 'API_KEY'
|
|
96
|
-
)
|
|
97
|
-
assert.ok(def, 'expected the secret definition to be inspected')
|
|
98
|
-
assert.equal(def!.docsUrl, undefined)
|
|
99
|
-
} finally {
|
|
100
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
test('carries docsUrl from defineVariable into the definition', async () => {
|
|
105
|
-
const rootDir = await mkdtemp(`${fixtureRoot}-variable-`)
|
|
106
|
-
const file = join(rootDir, 'variable.ts')
|
|
107
|
-
|
|
108
|
-
await writeFile(
|
|
109
|
-
file,
|
|
110
|
-
[
|
|
111
|
-
"import { defineVariable } from '@pikku/core'",
|
|
112
|
-
"import { z } from 'zod'",
|
|
113
|
-
'export const ConsoleUrlSchema = z.string()',
|
|
114
|
-
'defineVariable({',
|
|
115
|
-
" name: 'consoleUrl',",
|
|
116
|
-
" displayName: 'Console URL',",
|
|
117
|
-
" variableId: 'CONSOLE_URL',",
|
|
118
|
-
" docsUrl: 'https://example.com/docs/console-url',",
|
|
119
|
-
' schema: ConsoleUrlSchema,',
|
|
120
|
-
'})',
|
|
121
|
-
].join('\n')
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
125
|
-
try {
|
|
126
|
-
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
127
|
-
const def = state.variables.definitions.find(
|
|
128
|
-
(d) => d.variableId === 'CONSOLE_URL'
|
|
129
|
-
)
|
|
130
|
-
assert.ok(def, 'expected the variable definition to be inspected')
|
|
131
|
-
assert.equal(def!.docsUrl, 'https://example.com/docs/console-url')
|
|
132
|
-
} finally {
|
|
133
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
test('carries docsUrl from defineCredential into the definition', async () => {
|
|
138
|
-
const rootDir = await mkdtemp(`${fixtureRoot}-credential-`)
|
|
139
|
-
const file = join(rootDir, 'credential.ts')
|
|
140
|
-
|
|
141
|
-
await writeFile(
|
|
142
|
-
file,
|
|
143
|
-
[
|
|
144
|
-
"import { defineCredential } from '@pikku/core'",
|
|
145
|
-
"import { z } from 'zod'",
|
|
146
|
-
'export const TokenSchema = z.object({ token: z.string() })',
|
|
147
|
-
'defineCredential({',
|
|
148
|
-
" name: 'githubToken',",
|
|
149
|
-
" displayName: 'GitHub Token',",
|
|
150
|
-
" type: 'wire',",
|
|
151
|
-
" docsUrl: 'https://github.com/settings/tokens',",
|
|
152
|
-
' schema: TokenSchema,',
|
|
153
|
-
'})',
|
|
154
|
-
].join('\n')
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
158
|
-
try {
|
|
159
|
-
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
160
|
-
const def = state.credentials.definitions.find(
|
|
161
|
-
(d) => d.name === 'githubToken'
|
|
162
|
-
)
|
|
163
|
-
assert.ok(def, 'expected the credential definition to be inspected')
|
|
164
|
-
assert.equal(def!.docsUrl, 'https://github.com/settings/tokens')
|
|
165
|
-
} finally {
|
|
166
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
167
|
-
}
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* `optional` is the deploy gate's whole input. A flag the
|
|
173
|
-
* inspector drops leaves the gate seeing a bare declaration, which is how an
|
|
174
|
-
* optional secret suspends a deploy over a value production is meant NOT to
|
|
175
|
-
* have.
|
|
176
|
-
*/
|
|
177
|
-
describe('config value gating flags', () => {
|
|
178
|
-
test('carries optional from defineSecret into the definition', async () => {
|
|
179
|
-
const rootDir = await mkdtemp(`${fixtureRoot}-secret-optional-`)
|
|
180
|
-
const file = join(rootDir, 'secret.ts')
|
|
181
|
-
|
|
182
|
-
await writeFile(
|
|
183
|
-
file,
|
|
184
|
-
[
|
|
185
|
-
"import { defineSecret } from '@pikku/core'",
|
|
186
|
-
"import { z } from 'zod'",
|
|
187
|
-
'export const ActorSchema = z.string()',
|
|
188
|
-
'export const StripeSchema = z.string()',
|
|
189
|
-
'defineSecret({',
|
|
190
|
-
" name: 'scenarioActor',",
|
|
191
|
-
" displayName: 'Scenario Actor Secret',",
|
|
192
|
-
" secretId: 'SCENARIO_ACTOR_SECRET',",
|
|
193
|
-
' optional: true,',
|
|
194
|
-
' schema: ActorSchema,',
|
|
195
|
-
'})',
|
|
196
|
-
'defineSecret({',
|
|
197
|
-
" name: 'stripe',",
|
|
198
|
-
" displayName: 'Stripe',",
|
|
199
|
-
" secretId: 'STRIPE_KEY',",
|
|
200
|
-
' schema: StripeSchema,',
|
|
201
|
-
'})',
|
|
202
|
-
].join('\n')
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
206
|
-
try {
|
|
207
|
-
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
208
|
-
const optional = state.secrets.definitions.find(
|
|
209
|
-
(d) => d.secretId === 'SCENARIO_ACTOR_SECRET'
|
|
210
|
-
)
|
|
211
|
-
const required = state.secrets.definitions.find(
|
|
212
|
-
(d) => d.secretId === 'STRIPE_KEY'
|
|
213
|
-
)
|
|
214
|
-
assert.equal(optional!.optional, true)
|
|
215
|
-
assert.equal(required!.optional, undefined)
|
|
216
|
-
} finally {
|
|
217
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
test('carries optional from defineVariable into the definition', async () => {
|
|
222
|
-
const rootDir = await mkdtemp(`${fixtureRoot}-variable-optional-`)
|
|
223
|
-
const file = join(rootDir, 'variable.ts')
|
|
224
|
-
|
|
225
|
-
await writeFile(
|
|
226
|
-
file,
|
|
227
|
-
[
|
|
228
|
-
"import { defineVariable } from '@pikku/core'",
|
|
229
|
-
"import { z } from 'zod'",
|
|
230
|
-
'export const ConsoleUrlSchema = z.string()',
|
|
231
|
-
'export const CorsSchema = z.string()',
|
|
232
|
-
'defineVariable({',
|
|
233
|
-
" name: 'consoleUrl',",
|
|
234
|
-
" displayName: 'Console URL',",
|
|
235
|
-
" variableId: 'CONSOLE_URL',",
|
|
236
|
-
' optional: true,',
|
|
237
|
-
' schema: ConsoleUrlSchema,',
|
|
238
|
-
'})',
|
|
239
|
-
'defineVariable({',
|
|
240
|
-
" name: 'corsOrigins',",
|
|
241
|
-
" displayName: 'Allowed Browser Origins',",
|
|
242
|
-
" variableId: 'CORS_ORIGINS',",
|
|
243
|
-
' schema: CorsSchema,',
|
|
244
|
-
'})',
|
|
245
|
-
].join('\n')
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
249
|
-
try {
|
|
250
|
-
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
251
|
-
const optional = state.variables.definitions.find(
|
|
252
|
-
(d) => d.variableId === 'CONSOLE_URL'
|
|
253
|
-
)
|
|
254
|
-
const required = state.variables.definitions.find(
|
|
255
|
-
(d) => d.variableId === 'CORS_ORIGINS'
|
|
256
|
-
)
|
|
257
|
-
assert.equal(optional!.optional, true)
|
|
258
|
-
assert.equal(required!.optional, undefined)
|
|
259
|
-
} finally {
|
|
260
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
261
|
-
}
|
|
262
|
-
})
|
|
263
|
-
})
|