@pikku/inspector 0.12.72 → 0.12.74
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 +18 -0
- package/dist/add/add-functions.js +15 -3
- package/dist/inspector.js +19 -3
- package/dist/source-file-cache.d.ts +11 -0
- package/dist/source-file-cache.js +54 -0
- package/dist/types.d.ts +26 -0
- package/dist/utils/filter-inspector-state.js +13 -0
- package/package.json +1 -1
- package/src/add/add-functions.test.ts +98 -0
- package/src/add/add-functions.ts +16 -3
- package/src/inspector.ts +19 -2
- package/src/source-file-cache.ts +73 -0
- package/src/types.ts +27 -0
- package/src/utils/filter-inspector-state.test.ts +63 -0
- package/src/utils/filter-inspector-state.ts +13 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/check-secret-sinks.d.ts +0 -31
- package/dist/utils/check-secret-sinks.js +0 -118
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript';
|
|
2
|
-
export type SecretSinkViolation = {
|
|
3
|
-
/** How the value reaches the outside world, e.g. `logger.info`. */
|
|
4
|
-
sink: string;
|
|
5
|
-
/** Dotted path to the secret inside the argument, or `<argument>` when bare. */
|
|
6
|
-
path: string;
|
|
7
|
-
/** Position of the offending argument, for the diagnostic message. */
|
|
8
|
-
line: number;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Finds revealed vault secrets that flow into a sink.
|
|
12
|
-
*
|
|
13
|
-
* `SecretValue` is nominally typed, so the type system alone stops it reaching
|
|
14
|
-
* any of these. `.reveal()` is the deliberate escape hatch, and it hands back
|
|
15
|
-
* `Secret<T>` — the erasable classification brand. That brand is what this scan
|
|
16
|
-
* follows, which means it catches the revealed value both inline
|
|
17
|
-
* (`logger.info(s.reveal())`) and through a local binding, because the local's
|
|
18
|
-
* inferred type carries the brand too.
|
|
19
|
-
*
|
|
20
|
-
* The brand is optional by design, so an explicit annotation (`const t: string =
|
|
21
|
-
* s.reveal()`) erases it and this scan goes quiet. That is the documented limit
|
|
22
|
-
* of a structural marker: it catches the mistake, not someone determined to
|
|
23
|
-
* silence it.
|
|
24
|
-
*
|
|
25
|
-
* Sinks are matched on the receiver's resolved core interface, which means the
|
|
26
|
-
* scan needs the services parameter to be typed. Real projects get that from
|
|
27
|
-
* the generated `#pikku` factory; a hand-rolled file that calls the raw core
|
|
28
|
-
* factory without annotating its services leaves the parameter `any`, and an
|
|
29
|
-
* `any` receiver matches nothing.
|
|
30
|
-
*/
|
|
31
|
-
export declare function findRevealedSecretSinks(checker: ts.TypeChecker, handler: ts.Node): SecretSinkViolation[];
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript';
|
|
2
|
-
import { findPiiPaths } from './check-pii-output.js';
|
|
3
|
-
import { doesTypeExtendsCore } from './does-type-extend-core-type.js';
|
|
4
|
-
/**
|
|
5
|
-
* A method that writes its arguments somewhere a secret must never land, keyed
|
|
6
|
-
* by the core interface that owns it. `'*'` means every method on the type is a
|
|
7
|
-
* sink, which is true of a logger.
|
|
8
|
-
*/
|
|
9
|
-
const SINKS = [
|
|
10
|
-
{ coreType: 'Logger', methods: '*' },
|
|
11
|
-
{ coreType: 'QueueService', methods: ['add'] },
|
|
12
|
-
{ coreType: 'EmailService', methods: ['send'] },
|
|
13
|
-
{ coreType: 'WebhookService', methods: ['send'] },
|
|
14
|
-
{ coreType: 'AuditService', methods: ['audit', 'write'] },
|
|
15
|
-
{ coreType: 'AuditLog', methods: ['write'] },
|
|
16
|
-
];
|
|
17
|
-
/**
|
|
18
|
-
* Finds revealed vault secrets that flow into a sink.
|
|
19
|
-
*
|
|
20
|
-
* `SecretValue` is nominally typed, so the type system alone stops it reaching
|
|
21
|
-
* any of these. `.reveal()` is the deliberate escape hatch, and it hands back
|
|
22
|
-
* `Secret<T>` — the erasable classification brand. That brand is what this scan
|
|
23
|
-
* follows, which means it catches the revealed value both inline
|
|
24
|
-
* (`logger.info(s.reveal())`) and through a local binding, because the local's
|
|
25
|
-
* inferred type carries the brand too.
|
|
26
|
-
*
|
|
27
|
-
* The brand is optional by design, so an explicit annotation (`const t: string =
|
|
28
|
-
* s.reveal()`) erases it and this scan goes quiet. That is the documented limit
|
|
29
|
-
* of a structural marker: it catches the mistake, not someone determined to
|
|
30
|
-
* silence it.
|
|
31
|
-
*
|
|
32
|
-
* Sinks are matched on the receiver's resolved core interface, which means the
|
|
33
|
-
* scan needs the services parameter to be typed. Real projects get that from
|
|
34
|
-
* the generated `#pikku` factory; a hand-rolled file that calls the raw core
|
|
35
|
-
* factory without annotating its services leaves the parameter `any`, and an
|
|
36
|
-
* `any` receiver matches nothing.
|
|
37
|
-
*/
|
|
38
|
-
export function findRevealedSecretSinks(checker, handler) {
|
|
39
|
-
const violations = [];
|
|
40
|
-
const visit = (node) => {
|
|
41
|
-
if (ts.isCallExpression(node) &&
|
|
42
|
-
ts.isPropertyAccessExpression(node.expression)) {
|
|
43
|
-
const sink = describeSink(checker, node.expression);
|
|
44
|
-
if (sink) {
|
|
45
|
-
for (const arg of node.arguments) {
|
|
46
|
-
const found = findSecretExpression(checker, arg);
|
|
47
|
-
if (found) {
|
|
48
|
-
violations.push({
|
|
49
|
-
sink,
|
|
50
|
-
path: found.getText(),
|
|
51
|
-
line: found
|
|
52
|
-
.getSourceFile()
|
|
53
|
-
.getLineAndCharacterOfPosition(found.getStart()).line + 1,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
ts.forEachChild(node, visit);
|
|
60
|
-
};
|
|
61
|
-
visit(handler);
|
|
62
|
-
return violations;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Finds the first expression inside a sink argument whose own type carries the
|
|
66
|
-
* secret brand, and returns it so the diagnostic can quote it.
|
|
67
|
-
*
|
|
68
|
-
* This walks the argument rather than typing it whole, because an object
|
|
69
|
-
* literal in an argument position is contextually typed by the parameter — for
|
|
70
|
-
* a guarded sink that parameter is `Safe<M>`, and resolving the literal against
|
|
71
|
-
* it drops the optional brand before it can be read. Each inner expression
|
|
72
|
-
* (`token.reveal()`, or a reference to a local holding it) carries its own type
|
|
73
|
-
* regardless of the surrounding context, so reading those is what actually
|
|
74
|
-
* sees the secret.
|
|
75
|
-
*/
|
|
76
|
-
function findSecretExpression(checker, node) {
|
|
77
|
-
// Descend first, so an object literal wrapping the secret reports the
|
|
78
|
-
// expression that actually holds it (`token.reveal()`) rather than the whole
|
|
79
|
-
// literal. Nothing inside a `SecretValue` receiver is itself branded, so a
|
|
80
|
-
// bare `token.reveal()` still falls through to the self-check below.
|
|
81
|
-
for (const child of node.getChildren()) {
|
|
82
|
-
// A property name is not a value, and typing it re-resolves the property.
|
|
83
|
-
if (ts.isPropertyAssignment(node) && child === node.name)
|
|
84
|
-
continue;
|
|
85
|
-
const found = findSecretExpression(checker, child);
|
|
86
|
-
if (found)
|
|
87
|
-
return found;
|
|
88
|
-
}
|
|
89
|
-
if (ts.isExpression(node) && !ts.isStringLiteralLike(node)) {
|
|
90
|
-
const type = checker.getTypeAtLocation(node);
|
|
91
|
-
const isSecret = findPiiPaths(checker, type).some((f) => f.classification === 'secret');
|
|
92
|
-
if (isSecret)
|
|
93
|
-
return node;
|
|
94
|
-
}
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Names the sink a call writes to, or undefined when the call is not one.
|
|
99
|
-
* `console` is matched by identifier because it is a global with no Pikku type;
|
|
100
|
-
* everything else is matched on its resolved core interface so a renamed
|
|
101
|
-
* service or a decorator still counts.
|
|
102
|
-
*/
|
|
103
|
-
function describeSink(checker, callee) {
|
|
104
|
-
const method = callee.name.text;
|
|
105
|
-
const receiver = callee.expression;
|
|
106
|
-
if (ts.isIdentifier(receiver) && receiver.text === 'console') {
|
|
107
|
-
return `console.${method}`;
|
|
108
|
-
}
|
|
109
|
-
const receiverType = checker.getTypeAtLocation(receiver);
|
|
110
|
-
for (const { coreType, methods } of SINKS) {
|
|
111
|
-
if (methods !== '*' && !methods.includes(method))
|
|
112
|
-
continue;
|
|
113
|
-
if (doesTypeExtendsCore(receiverType, checker, new Set(), coreType)) {
|
|
114
|
-
return `${receiver.getText()}.${method}`;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|