@jinn-network/core 0.1.0 → 0.1.1-canary.7b188db5
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/dist/corpus-read/ipfs.d.ts +10 -1
- package/dist/corpus-read/ipfs.js +14 -5
- package/dist/scrub/apply-dispositions.d.ts +74 -0
- package/dist/scrub/apply-dispositions.js +309 -0
- package/dist/scrub/build.d.ts +54 -31
- package/dist/scrub/build.js +114 -55
- package/dist/scrub/checksummed-instruments-detector.d.ts +13 -0
- package/dist/scrub/checksummed-instruments-detector.js +119 -0
- package/dist/scrub/data/bip39-english.d.ts +2 -0
- package/dist/scrub/data/bip39-english.js +2051 -0
- package/dist/scrub/data/gitleaks-rules.d.ts +56 -0
- package/dist/scrub/data/gitleaks-rules.js +67 -0
- package/dist/scrub/emit-scrub.d.ts +13 -2
- package/dist/scrub/emit-scrub.js +39 -4
- package/dist/scrub/eval/findings-from-scrub.d.ts +18 -0
- package/dist/scrub/eval/findings-from-scrub.js +99 -0
- package/dist/scrub/eval/fixtures.d.ts +14 -0
- package/dist/scrub/eval/fixtures.js +159 -0
- package/dist/scrub/eval/index.d.ts +6 -0
- package/dist/scrub/eval/index.js +6 -0
- package/dist/scrub/eval/local-corpus.d.ts +15 -0
- package/dist/scrub/eval/local-corpus.js +29 -0
- package/dist/scrub/eval/metrics.d.ts +19 -0
- package/dist/scrub/eval/metrics.js +77 -0
- package/dist/scrub/eval/run-bench.d.ts +8 -0
- package/dist/scrub/eval/run-bench.js +125 -0
- package/dist/scrub/eval/types.d.ts +69 -0
- package/dist/scrub/eval/types.js +7 -0
- package/dist/scrub/finding.d.ts +38 -0
- package/dist/scrub/finding.js +7 -0
- package/dist/scrub/git-identity-detector.d.ts +16 -0
- package/dist/scrub/git-identity-detector.js +116 -0
- package/dist/scrub/gitleaks-detector.d.ts +13 -0
- package/dist/scrub/gitleaks-detector.js +45 -0
- package/dist/scrub/gliner-detector.d.ts +65 -0
- package/dist/scrub/gliner-detector.js +129 -0
- package/dist/scrub/index.d.ts +15 -1
- package/dist/scrub/index.js +15 -1
- package/dist/scrub/ip-address-detector.d.ts +12 -0
- package/dist/scrub/ip-address-detector.js +81 -0
- package/dist/scrub/key-policy.d.ts +18 -8
- package/dist/scrub/key-policy.js +49 -19
- package/dist/scrub/known-identity-detector.d.ts +66 -0
- package/dist/scrub/known-identity-detector.js +284 -0
- package/dist/scrub/layer2.d.ts +13 -18
- package/dist/scrub/layer2.js +22 -26
- package/dist/scrub/ml-pii-stage.d.ts +28 -10
- package/dist/scrub/ml-pii-stage.js +100 -23
- package/dist/scrub/pii-build.d.ts +17 -8
- package/dist/scrub/pii-build.js +9 -9
- package/dist/scrub/pipeline.d.ts +63 -24
- package/dist/scrub/pipeline.js +180 -38
- package/dist/scrub/plain-patterns-stage.d.ts +21 -19
- package/dist/scrub/plain-patterns-stage.js +61 -57
- package/dist/scrub/policy.d.ts +28 -0
- package/dist/scrub/policy.js +156 -0
- package/dist/scrub/provenance.d.ts +53 -0
- package/dist/scrub/provenance.js +83 -0
- package/dist/scrub/reject-classes-detector.d.ts +11 -0
- package/dist/scrub/reject-classes-detector.js +195 -0
- package/dist/scrub/reject-publish-error.d.ts +30 -0
- package/dist/scrub/reject-publish-error.js +47 -0
- package/dist/scrub/review-queue.d.ts +90 -0
- package/dist/scrub/review-queue.js +259 -0
- package/dist/scrub/secretlint-stage.d.ts +11 -26
- package/dist/scrub/secretlint-stage.js +45 -30
- package/dist/scrub/transformers-detector.d.ts +15 -9
- package/dist/scrub/transformers-detector.js +20 -10
- package/dist/scrub/types.d.ts +11 -0
- package/dist/scrub/url-credentials-detector.d.ts +9 -0
- package/dist/scrub/url-credentials-detector.js +66 -0
- package/dist/trajectory/schema.d.ts +75 -0
- package/dist/trajectory/schema.js +9 -0
- package/package.json +8 -3
- package/dist/scrub/openredaction-stage.d.ts +0 -196
- package/dist/scrub/openredaction-stage.js +0 -270
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export declare function normalizeIpfsGatewayBase(gatewayUrl: string): string;
|
|
2
2
|
export declare function buildIpfsHexCidCandidatesFromPartialHex(hex: string): string[];
|
|
3
3
|
export declare function buildIpfsFetchCidPathCandidates(cidOrPath: string): string[];
|
|
4
|
+
export type FetchFromIpfsOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Extra gateway base after the primary fails.
|
|
7
|
+
* - omit / undefined → production default `https://ipfs.io/ipfs/`
|
|
8
|
+
* - false → primary gateway only (hermetic / pinned rigs)
|
|
9
|
+
* - string → alternate fallback (normalized via `normalizeIpfsGatewayBase`)
|
|
10
|
+
*/
|
|
11
|
+
fallbackGatewayBase?: string | false;
|
|
12
|
+
};
|
|
4
13
|
/** Read-only multi-codec, primary-plus-fallback IPFS JSON fetch. */
|
|
5
|
-
export declare function fetchFromIpfs(gatewayUrl: string, cid: string): Promise<unknown>;
|
|
14
|
+
export declare function fetchFromIpfs(gatewayUrl: string, cid: string, opts?: FetchFromIpfsOptions): Promise<unknown>;
|
package/dist/corpus-read/ipfs.js
CHANGED
|
@@ -45,15 +45,24 @@ async function fetchJson(url, signal) {
|
|
|
45
45
|
throw new Error(`IPFS response is not JSON (content-type: ${contentType || 'none'})`);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
function resolveFallbackGatewayBases(opts) {
|
|
49
|
+
if (opts?.fallbackGatewayBase === false)
|
|
50
|
+
return [];
|
|
51
|
+
if (typeof opts?.fallbackGatewayBase === 'string') {
|
|
52
|
+
return [['fallback', normalizeIpfsGatewayBase(opts.fallbackGatewayBase)]];
|
|
53
|
+
}
|
|
54
|
+
return [['fallback', FALLBACK_IPFS_GATEWAY_BASE]];
|
|
55
|
+
}
|
|
48
56
|
/** Read-only multi-codec, primary-plus-fallback IPFS JSON fetch. */
|
|
49
|
-
export async function fetchFromIpfs(gatewayUrl, cid) {
|
|
57
|
+
export async function fetchFromIpfs(gatewayUrl, cid, opts) {
|
|
50
58
|
const primary = normalizeIpfsGatewayBase(gatewayUrl);
|
|
59
|
+
const gateways = [
|
|
60
|
+
['primary', primary],
|
|
61
|
+
...resolveFallbackGatewayBases(opts),
|
|
62
|
+
];
|
|
51
63
|
const errors = [];
|
|
52
64
|
for (const cidPath of buildIpfsFetchCidPathCandidates(cid)) {
|
|
53
|
-
for (const [name, baseUrl] of
|
|
54
|
-
['primary', primary],
|
|
55
|
-
['fallback', FALLBACK_IPFS_GATEWAY_BASE],
|
|
56
|
-
]) {
|
|
65
|
+
for (const [name, baseUrl] of gateways) {
|
|
57
66
|
const url = `${baseUrl}${cidPath}`;
|
|
58
67
|
const controller = new AbortController();
|
|
59
68
|
const timer = setTimeout(() => controller.abort(), IPFS_FETCH_TIMEOUT_MS);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply policy dispositions to an attribute bag after one detection pass
|
|
3
|
+
* (#1969 / design §6.5).
|
|
4
|
+
*/
|
|
5
|
+
import type { Finding } from './finding.js';
|
|
6
|
+
import { type PolicyTable } from './policy.js';
|
|
7
|
+
import { type ReviewQueueStore } from './review-queue.js';
|
|
8
|
+
import type { Attributes, RedactionRecord } from './types.js';
|
|
9
|
+
export { RejectPublishError, assertNoRejectPublish, rejectPublishFindings } from './reject-publish-error.js';
|
|
10
|
+
export interface ApplyDispositionsOptions {
|
|
11
|
+
policy?: PolicyTable;
|
|
12
|
+
/**
|
|
13
|
+
* Check-mode consumers (layer-2 distill, episode refuse): any non-pass
|
|
14
|
+
* disposition rejects. Still applies redacts so the body change is visible.
|
|
15
|
+
*/
|
|
16
|
+
checkMode?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Optional review-queue store. When provided, resolved flags are honored
|
|
19
|
+
* (`approve-instance` → pass, `redact-instance` → redact, allowlist /
|
|
20
|
+
* identity-pack → pass). Unresolved flags stay as `flag` and are listed in
|
|
21
|
+
* {@link ApplyDispositionsResult.unresolvedFlags}.
|
|
22
|
+
*/
|
|
23
|
+
reviewStore?: ReviewQueueStore;
|
|
24
|
+
}
|
|
25
|
+
export interface ApplyDispositionsResult {
|
|
26
|
+
attributes: Attributes;
|
|
27
|
+
redactions: RedactionRecord[];
|
|
28
|
+
findings: Finding[];
|
|
29
|
+
/** True when a reject-publish disposition fired, or check-mode saw a non-pass. */
|
|
30
|
+
rejected: boolean;
|
|
31
|
+
/** Flag findings that remain unresolved after consulting the review queue. */
|
|
32
|
+
unresolvedFlags: Finding[];
|
|
33
|
+
/**
|
|
34
|
+
* Applied disposition counts keyed `${ScrubClass}:${redact|flag|reject}`
|
|
35
|
+
* (#1974). Pass dispositions are omitted.
|
|
36
|
+
*/
|
|
37
|
+
perClassCounts: Record<string, number>;
|
|
38
|
+
}
|
|
39
|
+
/** Stub text for a redact disposition, keyed by class (+ optional evidence hint). */
|
|
40
|
+
export declare function stubForFinding(finding: Finding, occurrenceIndex: number): string;
|
|
41
|
+
/**
|
|
42
|
+
* Apply dispositions. Span redacts are applied right-to-left per key so earlier
|
|
43
|
+
* offsets stay valid. A5 reject-publish on a drop-key finding removes the key.
|
|
44
|
+
*/
|
|
45
|
+
export declare function applyDispositions(attributes: Attributes, findings: Finding[], opts?: ApplyDispositionsOptions): ApplyDispositionsResult;
|
|
46
|
+
/**
|
|
47
|
+
* Allowlist-pass records are auditable Legibility receipts, not detections.
|
|
48
|
+
* They must not trip refuse-on-detection / distill check-mode gates.
|
|
49
|
+
*/
|
|
50
|
+
export declare function countsTowardRefusal(record: RedactionRecord): boolean;
|
|
51
|
+
/** Redaction records that imply content was scrubbed or must refuse. */
|
|
52
|
+
export declare function refusalRedactions(redactions: RedactionRecord[]): RedactionRecord[];
|
|
53
|
+
/**
|
|
54
|
+
* True when publish/check should refuse the item.
|
|
55
|
+
*
|
|
56
|
+
* Prefer the explicit `rejected` bit (set by dispositions / check-mode).
|
|
57
|
+
* Fall back to refusal-relevant redaction records only — never `allowlist-pass`.
|
|
58
|
+
* For reject-publish classes (A4/A5 content), prefer
|
|
59
|
+
* {@link assertNoRejectPublish} so the abort is a loud class-named error.
|
|
60
|
+
* For unresolved flags, prefer {@link assertNoUnresolvedFlags} from
|
|
61
|
+
* `review-queue.ts`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function shouldRejectPublish(result: {
|
|
64
|
+
rejected?: boolean;
|
|
65
|
+
redactions: RedactionRecord[];
|
|
66
|
+
findings?: Finding[];
|
|
67
|
+
unresolvedFlags?: Finding[];
|
|
68
|
+
}): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Apply dispositions, then abort loudly when a reject-publish class fired.
|
|
71
|
+
* Use at publish altitude (capture/emit) — not for check-mode distill, which
|
|
72
|
+
* maps redactions to rejection reasons without throwing.
|
|
73
|
+
*/
|
|
74
|
+
export declare function applyDispositionsOrReject(attributes: Attributes, findings: Finding[], opts?: ApplyDispositionsOptions): ApplyDispositionsResult;
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply policy dispositions to an attribute bag after one detection pass
|
|
3
|
+
* (#1969 / design §6.5).
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_POLICY, checkModeRejects, resolveDisposition, } from './policy.js';
|
|
6
|
+
import { incrementPerClassCount } from './provenance.js';
|
|
7
|
+
import { assertNoRejectPublish } from './reject-publish-error.js';
|
|
8
|
+
import { findingFingerprint, } from './review-queue.js';
|
|
9
|
+
export { RejectPublishError, assertNoRejectPublish, rejectPublishFindings } from './reject-publish-error.js';
|
|
10
|
+
/** Stub text for a redact disposition, keyed by class (+ optional evidence hint). */
|
|
11
|
+
export function stubForFinding(finding, occurrenceIndex) {
|
|
12
|
+
const hint = finding.evidence[0] ?? '';
|
|
13
|
+
switch (finding.class) {
|
|
14
|
+
case 'B1':
|
|
15
|
+
return '[EMAIL]';
|
|
16
|
+
case 'B2':
|
|
17
|
+
// Joint carrier: user.email values stub as email; names as NAME.
|
|
18
|
+
if (hint.includes('email'))
|
|
19
|
+
return '[EMAIL]';
|
|
20
|
+
return '[NAME]';
|
|
21
|
+
case 'D1':
|
|
22
|
+
return '/users/anon';
|
|
23
|
+
case 'C1':
|
|
24
|
+
return `[ETH_ADDR_${occurrenceIndex}]`;
|
|
25
|
+
case 'B3':
|
|
26
|
+
return '[NAME]';
|
|
27
|
+
case 'B4':
|
|
28
|
+
return '[USERNAME]';
|
|
29
|
+
case 'D2':
|
|
30
|
+
return '[IP]';
|
|
31
|
+
case 'D3':
|
|
32
|
+
return '[HOSTNAME]';
|
|
33
|
+
case 'A1':
|
|
34
|
+
if (hint.includes('aws-access-key-id'))
|
|
35
|
+
return '[SECRET:aws-access-key-id]';
|
|
36
|
+
if (hint.includes('gcp-api-key'))
|
|
37
|
+
return '[SECRET:gcp-api-key]';
|
|
38
|
+
if (hint.startsWith('secret:'))
|
|
39
|
+
return `[SECRET:${hint.slice('secret:'.length)}]`;
|
|
40
|
+
return '[SECRET:redacted]';
|
|
41
|
+
case 'A2':
|
|
42
|
+
return '[SECRET:high-entropy]';
|
|
43
|
+
case 'A3':
|
|
44
|
+
return '[SECRET:url-credential]';
|
|
45
|
+
case 'B7':
|
|
46
|
+
if (hint.includes('iban'))
|
|
47
|
+
return '[IBAN]';
|
|
48
|
+
return '[CARD]';
|
|
49
|
+
default: {
|
|
50
|
+
if (hint.startsWith('ml:')) {
|
|
51
|
+
return `[PII:${hint.slice('ml:'.length)}]`;
|
|
52
|
+
}
|
|
53
|
+
if (hint.startsWith('gitleaks:')) {
|
|
54
|
+
return `[SECRET:${hint.slice('gitleaks:'.length)}]`;
|
|
55
|
+
}
|
|
56
|
+
const label = hint || finding.class;
|
|
57
|
+
return `[PII:${label}]`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function redactionKind(scrubClass, _finding) {
|
|
62
|
+
if (scrubClass.startsWith('A'))
|
|
63
|
+
return 'secret';
|
|
64
|
+
return 'pii';
|
|
65
|
+
}
|
|
66
|
+
function redactionDetail(finding) {
|
|
67
|
+
const hint = finding.evidence[0] ?? finding.class;
|
|
68
|
+
if (hint === 'drop-key' || hint === 'machine-identity-key')
|
|
69
|
+
return undefined;
|
|
70
|
+
// Normalize C1 to the shipped detail id so seed/trace/layer2 records match.
|
|
71
|
+
if (finding.class === 'C1')
|
|
72
|
+
return 'eth-address';
|
|
73
|
+
if (hint.startsWith('secret:'))
|
|
74
|
+
return hint.slice('secret:'.length);
|
|
75
|
+
if (hint.startsWith('gitleaks:'))
|
|
76
|
+
return hint.slice('gitleaks:'.length);
|
|
77
|
+
if (hint.startsWith('ml:'))
|
|
78
|
+
return hint.slice('ml:'.length);
|
|
79
|
+
return hint;
|
|
80
|
+
}
|
|
81
|
+
function isKeyDropFinding(finding) {
|
|
82
|
+
return (finding.evidence.includes('drop-key') ||
|
|
83
|
+
finding.evidence.includes('machine-identity-key'));
|
|
84
|
+
}
|
|
85
|
+
function decisionOverridesFlag(decision) {
|
|
86
|
+
if (decision === 'redact-instance')
|
|
87
|
+
return 'redact';
|
|
88
|
+
if (decision === 'approve-instance' ||
|
|
89
|
+
decision === 'add-to-allowlist' ||
|
|
90
|
+
decision === 'add-to-identity-pack') {
|
|
91
|
+
return 'pass';
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function effectiveDisposition(finding, policy, checkMode, reviewStore) {
|
|
96
|
+
let disposition = resolveDisposition(finding.class, finding.confidence, policy);
|
|
97
|
+
if (disposition === 'flag' && reviewStore) {
|
|
98
|
+
const override = decisionOverridesFlag(reviewStore.resolutionFor(finding));
|
|
99
|
+
if (override)
|
|
100
|
+
disposition = override;
|
|
101
|
+
}
|
|
102
|
+
if (checkMode && disposition !== 'pass') {
|
|
103
|
+
// Check-mode still applies the underlying redact/reject action; the
|
|
104
|
+
// rejected bit is set separately via checkModeRejects.
|
|
105
|
+
return disposition;
|
|
106
|
+
}
|
|
107
|
+
return disposition;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Apply dispositions. Span redacts are applied right-to-left per key so earlier
|
|
111
|
+
* offsets stay valid. A5 reject-publish on a drop-key finding removes the key.
|
|
112
|
+
*/
|
|
113
|
+
export function applyDispositions(attributes, findings, opts = {}) {
|
|
114
|
+
const policy = opts.policy ?? DEFAULT_POLICY;
|
|
115
|
+
const checkMode = opts.checkMode ?? false;
|
|
116
|
+
const reviewStore = opts.reviewStore;
|
|
117
|
+
const out = { ...attributes };
|
|
118
|
+
const redactions = [];
|
|
119
|
+
const unresolvedFlags = [];
|
|
120
|
+
const perClassCounts = {};
|
|
121
|
+
let rejected = false;
|
|
122
|
+
// Key-drop findings first (A5 structural drop-key, D3 machine-identity).
|
|
123
|
+
// Structural key drops remove the attribute; they do NOT reject-publish in
|
|
124
|
+
// redact-mode (content-level A4/A5 spans own the loud abort). Check-mode
|
|
125
|
+
// still rejects so distill stays fail-closed on any non-pass.
|
|
126
|
+
for (const finding of findings) {
|
|
127
|
+
if (!isKeyDropFinding(finding))
|
|
128
|
+
continue;
|
|
129
|
+
const disposition = effectiveDisposition(finding, policy, checkMode, reviewStore);
|
|
130
|
+
if (disposition === 'pass')
|
|
131
|
+
continue;
|
|
132
|
+
delete out[finding.span.key];
|
|
133
|
+
redactions.push({
|
|
134
|
+
key: finding.span.key,
|
|
135
|
+
stage: finding.detector.name,
|
|
136
|
+
kind: 'dropped-key',
|
|
137
|
+
});
|
|
138
|
+
// Structural drops are applied as redacts (key removed), even when the
|
|
139
|
+
// policy row for A5 content is reject-publish.
|
|
140
|
+
incrementPerClassCount(perClassCounts, finding.class, 'redact');
|
|
141
|
+
if (checkMode)
|
|
142
|
+
rejected = true;
|
|
143
|
+
}
|
|
144
|
+
// Group remaining span findings by key, apply right-to-left.
|
|
145
|
+
const byKey = new Map();
|
|
146
|
+
for (const finding of findings) {
|
|
147
|
+
if (isKeyDropFinding(finding))
|
|
148
|
+
continue;
|
|
149
|
+
if (!(finding.span.key in out))
|
|
150
|
+
continue;
|
|
151
|
+
const list = byKey.get(finding.span.key) ?? [];
|
|
152
|
+
list.push(finding);
|
|
153
|
+
byKey.set(finding.span.key, list);
|
|
154
|
+
}
|
|
155
|
+
for (const [key, keyFindings] of byKey) {
|
|
156
|
+
const value = out[key];
|
|
157
|
+
if (typeof value !== 'string')
|
|
158
|
+
continue;
|
|
159
|
+
const sorted = [...keyFindings].sort((a, b) => b.span.start - a.span.start);
|
|
160
|
+
const covered = [];
|
|
161
|
+
let text = value;
|
|
162
|
+
// Collect redact records left-to-right (unshift while applying right-to-left)
|
|
163
|
+
// so callers that pin redaction order keep a stable, start-ascending list.
|
|
164
|
+
const redactRecords = [];
|
|
165
|
+
for (const finding of sorted) {
|
|
166
|
+
const disposition = effectiveDisposition(finding, policy, checkMode, reviewStore);
|
|
167
|
+
if (disposition === 'pass') {
|
|
168
|
+
// Auditable allowlist hit (§6.4): record "we saw it and passed it on
|
|
169
|
+
// purpose". These must NOT count toward refuse-on-detection /
|
|
170
|
+
// check-mode rejection (kind `allowlist-pass` is filtered by
|
|
171
|
+
// {@link countsTowardRefusal}).
|
|
172
|
+
const allowEvidence = finding.evidence.find((e) => e.startsWith('allowlist:'));
|
|
173
|
+
if (allowEvidence) {
|
|
174
|
+
redactions.push({
|
|
175
|
+
key,
|
|
176
|
+
stage: finding.detector.name,
|
|
177
|
+
kind: 'allowlist-pass',
|
|
178
|
+
detail: allowEvidence.slice('allowlist:'.length),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (disposition === 'reject-publish') {
|
|
184
|
+
// Always stub the span so A4/A5 plaintext never survives into
|
|
185
|
+
// check-mode / in-memory capture envelopes; abort is still signaled
|
|
186
|
+
// via `rejected` + assertNoRejectPublish at publish altitude.
|
|
187
|
+
rejected = true;
|
|
188
|
+
const { start, end } = finding.span;
|
|
189
|
+
if (start >= 0 && end <= text.length && start < end &&
|
|
190
|
+
!covered.some((c) => start < c.end && end > c.start)) {
|
|
191
|
+
const leftIndex = keyFindings.filter((f) => {
|
|
192
|
+
const d = effectiveDisposition(f, policy, checkMode, reviewStore);
|
|
193
|
+
return (d === 'redact' || d === 'reject-publish') && f.span.start < start;
|
|
194
|
+
}).length;
|
|
195
|
+
const stub = stubForFinding(finding, leftIndex);
|
|
196
|
+
text = text.slice(0, start) + stub + text.slice(end);
|
|
197
|
+
covered.push({ start, end });
|
|
198
|
+
}
|
|
199
|
+
redactRecords.unshift({
|
|
200
|
+
key,
|
|
201
|
+
stage: finding.detector.name,
|
|
202
|
+
kind: redactionKind(finding.class, finding),
|
|
203
|
+
detail: redactionDetail(finding),
|
|
204
|
+
});
|
|
205
|
+
incrementPerClassCount(perClassCounts, finding.class, disposition);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (disposition === 'flag') {
|
|
209
|
+
unresolvedFlags.push(finding);
|
|
210
|
+
incrementPerClassCount(perClassCounts, finding.class, disposition);
|
|
211
|
+
// In redact-mode leave text (review queue owns the hold). In check-mode,
|
|
212
|
+
// record so consumers reject.
|
|
213
|
+
if (checkMode) {
|
|
214
|
+
rejected = true;
|
|
215
|
+
redactions.push({
|
|
216
|
+
key,
|
|
217
|
+
stage: finding.detector.name,
|
|
218
|
+
kind: 'flag',
|
|
219
|
+
detail: redactionDetail(finding),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// redact
|
|
225
|
+
const { start, end } = finding.span;
|
|
226
|
+
if (start < 0 || end > text.length || start >= end)
|
|
227
|
+
continue;
|
|
228
|
+
if (covered.some((c) => start < c.end && end > c.start))
|
|
229
|
+
continue;
|
|
230
|
+
const leftIndex = keyFindings.filter((f) => {
|
|
231
|
+
if (effectiveDisposition(f, policy, checkMode, reviewStore) !== 'redact')
|
|
232
|
+
return false;
|
|
233
|
+
return f.span.start < start;
|
|
234
|
+
}).length;
|
|
235
|
+
const stub = stubForFinding(finding, leftIndex);
|
|
236
|
+
text = text.slice(0, start) + stub + text.slice(end);
|
|
237
|
+
covered.push({ start, end });
|
|
238
|
+
redactRecords.unshift({
|
|
239
|
+
key,
|
|
240
|
+
stage: finding.detector.name,
|
|
241
|
+
kind: redactionKind(finding.class, finding),
|
|
242
|
+
detail: redactionDetail(finding),
|
|
243
|
+
});
|
|
244
|
+
incrementPerClassCount(perClassCounts, finding.class, disposition);
|
|
245
|
+
if (checkMode)
|
|
246
|
+
rejected = true;
|
|
247
|
+
}
|
|
248
|
+
redactions.push(...redactRecords);
|
|
249
|
+
out[key] = text;
|
|
250
|
+
}
|
|
251
|
+
if (checkMode && checkModeRejects(findings, policy)) {
|
|
252
|
+
rejected = true;
|
|
253
|
+
}
|
|
254
|
+
// Dedupe unresolved flags by fingerprint (nested walks can re-emit).
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
const uniqueUnresolved = unresolvedFlags.filter((f) => {
|
|
257
|
+
const fp = findingFingerprint(f);
|
|
258
|
+
if (seen.has(fp))
|
|
259
|
+
return false;
|
|
260
|
+
seen.add(fp);
|
|
261
|
+
return true;
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
attributes: out,
|
|
265
|
+
redactions,
|
|
266
|
+
findings,
|
|
267
|
+
rejected,
|
|
268
|
+
unresolvedFlags: uniqueUnresolved,
|
|
269
|
+
perClassCounts,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Allowlist-pass records are auditable Legibility receipts, not detections.
|
|
274
|
+
* They must not trip refuse-on-detection / distill check-mode gates.
|
|
275
|
+
*/
|
|
276
|
+
export function countsTowardRefusal(record) {
|
|
277
|
+
return record.kind !== 'allowlist-pass';
|
|
278
|
+
}
|
|
279
|
+
/** Redaction records that imply content was scrubbed or must refuse. */
|
|
280
|
+
export function refusalRedactions(redactions) {
|
|
281
|
+
return redactions.filter(countsTowardRefusal);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* True when publish/check should refuse the item.
|
|
285
|
+
*
|
|
286
|
+
* Prefer the explicit `rejected` bit (set by dispositions / check-mode).
|
|
287
|
+
* Fall back to refusal-relevant redaction records only — never `allowlist-pass`.
|
|
288
|
+
* For reject-publish classes (A4/A5 content), prefer
|
|
289
|
+
* {@link assertNoRejectPublish} so the abort is a loud class-named error.
|
|
290
|
+
* For unresolved flags, prefer {@link assertNoUnresolvedFlags} from
|
|
291
|
+
* `review-queue.ts`.
|
|
292
|
+
*/
|
|
293
|
+
export function shouldRejectPublish(result) {
|
|
294
|
+
if (result.rejected)
|
|
295
|
+
return true;
|
|
296
|
+
if (result.unresolvedFlags && result.unresolvedFlags.length > 0)
|
|
297
|
+
return true;
|
|
298
|
+
return refusalRedactions(result.redactions).length > 0;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Apply dispositions, then abort loudly when a reject-publish class fired.
|
|
302
|
+
* Use at publish altitude (capture/emit) — not for check-mode distill, which
|
|
303
|
+
* maps redactions to rejection reasons without throwing.
|
|
304
|
+
*/
|
|
305
|
+
export function applyDispositionsOrReject(attributes, findings, opts = {}) {
|
|
306
|
+
const applied = applyDispositions(attributes, findings, opts);
|
|
307
|
+
assertNoRejectPublish(applied, opts.policy ?? DEFAULT_POLICY);
|
|
308
|
+
return applied;
|
|
309
|
+
}
|
package/dist/scrub/build.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ScrubPipeline } from './pipeline.js';
|
|
2
2
|
import { type KeyPolicy } from './key-policy.js';
|
|
3
3
|
import { type PiiDetector } from './ml-pii-stage.js';
|
|
4
|
+
import { type AssembleKnownIdentityOptions, type AssembledKnownIdentity } from './known-identity-detector.js';
|
|
5
|
+
import type { Detector } from './finding.js';
|
|
4
6
|
/**
|
|
5
7
|
* Default key policy. `jinn.*` identity/chain attributes are structural and pass
|
|
6
8
|
* raw. The `drop` tier deletes the spec's stage-1 high-confidence keys outright
|
|
@@ -8,45 +10,66 @@ import { type PiiDetector } from './ml-pii-stage.js';
|
|
|
8
10
|
* these never carry sellable content and are never safe to publish. Globs use the
|
|
9
11
|
* trailing-`*` prefix form `classifyKey` supports; the HTTP header keys are listed
|
|
10
12
|
* per request/response direction (a leading `*.header.…` glob is not matched, so
|
|
11
|
-
* we enumerate the concrete keys).
|
|
12
|
-
*
|
|
13
|
+
* we enumerate the concrete keys). Machine-identity keys (D3 carrier) drop
|
|
14
|
+
* attempt-manifest `host` / hostname telemetry. Everything else is `content` and
|
|
15
|
+
* flows through the value-scrubbing detectors.
|
|
13
16
|
*/
|
|
14
17
|
export declare const DEFAULT_KEY_POLICY: KeyPolicy;
|
|
15
18
|
export interface BuildScrubPipelineOptions {
|
|
16
19
|
policy?: KeyPolicy;
|
|
17
|
-
/** When provided, the ML PII (GLiNER)
|
|
20
|
+
/** When provided, the ML PII (GLiNER) detector is appended. */
|
|
18
21
|
piiDetector?: PiiDetector;
|
|
22
|
+
/** Known-identity pack + non-address allowlist (#1971). */
|
|
23
|
+
knownIdentity?: AssembleKnownIdentityOptions | AssembledKnownIdentity;
|
|
24
|
+
/** Review-queue store for flag resolutions (#1973). */
|
|
25
|
+
reviewStore?: import('./review-queue.js').ReviewQueueStore;
|
|
26
|
+
/**
|
|
27
|
+
* When false, unresolved flags do not abort (tests). Default true for
|
|
28
|
+
* redact-mode publish lanes.
|
|
29
|
+
*/
|
|
30
|
+
failClosedOnUnresolvedFlags?: boolean;
|
|
19
31
|
}
|
|
32
|
+
export declare function resolveKnownIdentity(opts?: AssembleKnownIdentityOptions | AssembledKnownIdentity): AssembledKnownIdentity;
|
|
33
|
+
/** Build provenance extras from known-identity + optional ML detector (#1974). */
|
|
34
|
+
export declare function buildProvenanceExtras(knownIdentity: AssembledKnownIdentity, piiDetector?: PiiDetector): {
|
|
35
|
+
modelId: string | null;
|
|
36
|
+
labels: readonly string[];
|
|
37
|
+
allowlistDigest: string;
|
|
38
|
+
};
|
|
20
39
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
40
|
+
* Shared detector inventory (#1969 / #1970 / #1971 / #1972 / #1973). Every
|
|
41
|
+
* publish/check consumer runs the same owned detectors (key-policy,
|
|
42
|
+
* plain-patterns including C1 wallet + A1 credential IDs, git-identity B2
|
|
43
|
+
* carriers, known-identity pack + non-address allowlist, Tier-1
|
|
44
|
+
* reject/URL/IP/instruments/gitleaks, secretlint). What varies is disposition /
|
|
45
|
+
* check-mode and whether an ML PII detector is injected. Seed keeps the entropy
|
|
46
|
+
* fallback off until A2 mid-band flags are absorbed without refuse-on-detection
|
|
47
|
+
* (#1409 shipped behavior). openredaction was retired in #1973.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sharedDetectorInventory(policy: KeyPolicy, opts?: {
|
|
50
|
+
entropyFallback?: boolean;
|
|
51
|
+
piiDetector?: PiiDetector;
|
|
52
|
+
knownIdentity?: AssembleKnownIdentityOptions | AssembledKnownIdentity;
|
|
53
|
+
}): Detector[];
|
|
54
|
+
/**
|
|
55
|
+
* Trace / redact-mode preset over the shared inventory.
|
|
56
|
+
*
|
|
57
|
+
* @deprecated Prefer thinking in terms of one inventory + policy; this builder
|
|
58
|
+
* remains as a compatibility preset through the migration (#1969).
|
|
26
59
|
*/
|
|
27
60
|
export declare function buildScrubPipeline(opts?: BuildScrubPipelineOptions): ScrubPipeline;
|
|
61
|
+
export interface BuildSeedScrubPipelineOptions {
|
|
62
|
+
policy?: KeyPolicy;
|
|
63
|
+
knownIdentity?: AssembleKnownIdentityOptions | AssembledKnownIdentity;
|
|
64
|
+
piiDetector?: PiiDetector;
|
|
65
|
+
reviewStore?: import('./review-queue.js').ReviewQueueStore;
|
|
66
|
+
failClosedOnUnresolvedFlags?: boolean;
|
|
67
|
+
}
|
|
28
68
|
/**
|
|
29
|
-
* Seed-
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* long camelCase identifiers) and deface the corpus. The deterministic
|
|
35
|
-
* detectors stay:
|
|
36
|
-
* structural key policy, plain-patterns (emails, home paths, and — seed-only,
|
|
37
|
-
* #1415 — bare AWS access-key IDs and GCP `AIza…` API keys, deterministic
|
|
38
|
-
* prefix shapes secretlint pass-1 does not cover), and secretlint's pass-1
|
|
39
|
-
* preset rules (AWS secret-key assignments, GitHub / Slack / npm token
|
|
40
|
-
* shapes, GCP service-account JSON). Accepted residual: JWTs and unprefixed
|
|
41
|
-
* high-entropy blobs (trace profile catches them via the entropy fallback),
|
|
42
|
-
* plus every structured identifier or PII class detected only by the omitted
|
|
43
|
-
* openredaction stage, pass unredacted. The latter is a 570+ pattern surface;
|
|
44
|
-
* payment cards, phone numbers, SSNs, medical or health-plan identifiers,
|
|
45
|
-
* government identity documents, and financial account references are
|
|
46
|
-
* examples, not an exhaustive allowlist. This is acceptable only for public
|
|
47
|
-
* seed material that a curator has transformed and reviewed for those classes.
|
|
48
|
-
* The reduced stage list is inspectable locally through the pipeline's
|
|
49
|
-
* `components` surface. `TraceEnvelopeV0` does not publish that list, so a
|
|
50
|
-
* fetched envelope cannot by itself prove which scrub profile ran.
|
|
69
|
+
* Seed / redact-mode preset (#1409 / #1969). Same owned inventory as layer-2.
|
|
70
|
+
* Entropy fallback stays off to preserve the shipped zero-corruption seed
|
|
71
|
+
* behavior until A2 mid-band can flag without tripping refuse-on-detection.
|
|
72
|
+
*
|
|
73
|
+
* @deprecated Compatibility preset over the one inventory + policy table.
|
|
51
74
|
*/
|
|
52
|
-
export declare function buildSeedScrubPipeline(
|
|
75
|
+
export declare function buildSeedScrubPipeline(policyOrOpts?: KeyPolicy | BuildSeedScrubPipelineOptions): ScrubPipeline;
|