@codyswann/lisa 4.70.3 → 4.71.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/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +174 -0
- package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +279 -0
- package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +17 -7
- package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +241 -0
- package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +283 -0
- package/all/copy-overwrite/scripts/lib/github-attestation-verifier.mjs +244 -0
- package/all/copy-overwrite/scripts/lib/history-secret-evidence-shape.mjs +188 -0
- package/all/copy-overwrite/scripts/lib/history-secret-evidence.mjs +271 -0
- package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +6 -296
- package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +289 -0
- package/all/copy-overwrite/scripts/lib/npm-update-authorization.mjs +235 -0
- package/all/copy-overwrite/scripts/lib/npm-update-broker-client.mjs +92 -0
- package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +241 -0
- package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +278 -0
- package/all/copy-overwrite/scripts/lib/npm-update-controller-broker.mjs +234 -0
- package/all/copy-overwrite/scripts/lib/npm-update-controller-factory.mjs +278 -0
- package/all/copy-overwrite/scripts/lib/npm-update-controller-recipe.mjs +135 -0
- package/all/copy-overwrite/scripts/lib/npm-update-execution-adapter.mjs +292 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +214 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate-install.mjs +267 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate-proof.mjs +99 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +300 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gh-dispatch.mjs +182 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +156 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +221 -0
- package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +214 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper-graph.mjs +238 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +126 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +292 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +116 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-preload.mjs +153 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-provider.mjs +207 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-read-client.mjs +164 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +272 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +157 -0
- package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +28 -0
- package/all/copy-overwrite/scripts/lib/npm-update-isolation.mjs +11 -0
- package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +171 -0
- package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +194 -0
- package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +155 -0
- package/all/copy-overwrite/scripts/lib/npm-update-npm.mjs +190 -0
- package/all/copy-overwrite/scripts/lib/npm-update-object.mjs +112 -0
- package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +273 -0
- package/all/copy-overwrite/scripts/lib/npm-update-owner.mjs +292 -0
- package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +263 -0
- package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +179 -0
- package/all/copy-overwrite/scripts/lib/npm-update-process.mjs +16 -0
- package/all/copy-overwrite/scripts/lib/npm-update-publication.mjs +258 -0
- package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +258 -0
- package/all/copy-overwrite/scripts/lib/npm-update-quality.mjs +153 -0
- package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +264 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime-archive.mjs +204 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime-graph.mjs +185 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime-transport.mjs +221 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime.mjs +284 -0
- package/all/copy-overwrite/scripts/lib/npm-update-tool-launcher.mjs +287 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-environment.mjs +161 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +142 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +256 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-policy.mjs +266 -0
- package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +230 -36
- package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +161 -0
- package/all/copy-overwrite/scripts/lisa-history-secrets.mjs +8 -1
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +89 -5
- package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +204 -0
- package/all/copy-overwrite/scripts/lisa-run-gates.mjs +48 -123
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +107 -41
- package/all/copy-overwrite/scripts/npm-updater-gate-runtime.json +1 -0
- package/all/copy-overwrite/scripts/npm-updater-gate-supervisor.c +121 -0
- package/all/copy-overwrite/scripts/npm-updater-gate.Dockerfile +49 -0
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1085 -0
- package/dist/codex/scripts/block-no-verify.sh +89 -5
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +620 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +165 -13
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-no-verify.sh +89 -5
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/block-no-verify.agy.sh +89 -5
- package/plugins/lisa/hooks/block-no-verify.sh +89 -5
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +89 -5
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +89 -5
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +89 -5
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-no-verify.agy.sh +89 -5
- package/plugins/src/base/hooks/block-no-verify.sh +89 -5
- package/scripts/check-shipped-surface-removals.mjs +96 -24
- package/scripts/generate-npm-updater-helper-graph.mjs +184 -0
- package/scripts/two-channel-couplings.json +31 -7
- package/typescript/copy-contents/.husky/commit-msg +18 -0
- package/typescript/copy-overwrite/.prettierignore +2 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/** Bounded official verification followed by exact authorization policy. */
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
7
|
+
import { isAbsolute } from "node:path";
|
|
8
|
+
import { boundedSpawnSync } from "./bounded-spawn.mjs";
|
|
9
|
+
|
|
10
|
+
export const ACTIONS_ISSUER = "https://token.actions.githubusercontent.com";
|
|
11
|
+
export const PROPOSAL_PREDICATE =
|
|
12
|
+
"https://lisa.dev/attestations/npm-proposal/v1";
|
|
13
|
+
export const RECOVERY_PREDICATE =
|
|
14
|
+
"https://lisa.dev/attestations/npm-recovery/v1";
|
|
15
|
+
|
|
16
|
+
/** Hash exact bytes; never an estimated identifier. */
|
|
17
|
+
export function sha256(value) {
|
|
18
|
+
return createHash("sha256").update(value).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Require a predicate, hiding potentially sensitive provider output. */
|
|
22
|
+
export function requireProof(condition, reason) {
|
|
23
|
+
if (!condition) throw new Error(`Invalid automation provenance: ${reason}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Run only the trusted, hash-pinned official executable with bounded output. */
|
|
27
|
+
export function assertPinnedVerifier(policy) {
|
|
28
|
+
requireProof(
|
|
29
|
+
isAbsolute(policy.ghExecutable ?? ""),
|
|
30
|
+
"official gh executable is not pinned"
|
|
31
|
+
);
|
|
32
|
+
const stat = lstatSync(policy.ghExecutable);
|
|
33
|
+
requireProof(
|
|
34
|
+
stat.isFile() && stat.size <= 150_000_000,
|
|
35
|
+
"invalid official gh file"
|
|
36
|
+
);
|
|
37
|
+
requireProof(
|
|
38
|
+
sha256(readFileSync(policy.ghExecutable)) === policy.ghSha256,
|
|
39
|
+
"official gh bytes differ"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Run only the already pinned verifier/provider reader under fixed bounds. */
|
|
44
|
+
export function ghJson(policy, args, execute = boundedSpawnSync) {
|
|
45
|
+
assertPinnedVerifier(policy);
|
|
46
|
+
const result = execute(policy.ghExecutable, args, {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
timeout: 30_000,
|
|
49
|
+
maxBuffer: 1_048_576,
|
|
50
|
+
env: { ...process.env, GH_HOST: "github.com" },
|
|
51
|
+
});
|
|
52
|
+
requireProof(
|
|
53
|
+
!result.error && !result.signal && result.status === 0,
|
|
54
|
+
"official verifier/provider request failed"
|
|
55
|
+
);
|
|
56
|
+
requireProof(
|
|
57
|
+
typeof result.stdout === "string" &&
|
|
58
|
+
Buffer.byteLength(result.stdout) <= 1_048_576,
|
|
59
|
+
"oversize verifier result"
|
|
60
|
+
);
|
|
61
|
+
return JSON.parse(result.stdout);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Strictly match fields from a successfully verified certificate. */
|
|
65
|
+
function matchCertificate(certificate, descriptor, policy) {
|
|
66
|
+
const expected = {
|
|
67
|
+
issuer: ACTIONS_ISSUER,
|
|
68
|
+
buildSignerURI: `https://github.com/${policy.signerWorkflow}@${policy.signerDigest}`,
|
|
69
|
+
buildSignerDigest: policy.signerDigest,
|
|
70
|
+
buildConfigURI: `https://github.com/${policy.callerWorkflow}@refs/heads/main`,
|
|
71
|
+
buildConfigDigest: descriptor.parent,
|
|
72
|
+
sourceRepositoryURI: `https://github.com/${policy.repository}`,
|
|
73
|
+
sourceRepositoryDigest: descriptor.parent,
|
|
74
|
+
sourceRepositoryRef: "refs/heads/main",
|
|
75
|
+
sourceRepositoryIdentifier: policy.repositoryId,
|
|
76
|
+
sourceRepositoryOwnerIdentifier: policy.ownerId,
|
|
77
|
+
runInvocationURI: `https://github.com/${policy.repository}/actions/runs/${descriptor.runId}/attempts/${descriptor.runAttempt}`,
|
|
78
|
+
runnerEnvironment: "github-hosted",
|
|
79
|
+
};
|
|
80
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
81
|
+
requireProof(
|
|
82
|
+
typeof value === "string" && value !== "" && certificate?.[key] === value,
|
|
83
|
+
`certificate ${key} differs`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
requireProof(
|
|
87
|
+
policy.allowedTriggers.includes(certificate.buildTrigger),
|
|
88
|
+
"unapproved workflow trigger"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Only authenticated observer times count; current-time-only is insufficient. */
|
|
93
|
+
export function matchSigningTime(timestamps, policy, now) {
|
|
94
|
+
requireProof(
|
|
95
|
+
Array.isArray(timestamps) &&
|
|
96
|
+
timestamps.length > 0 &&
|
|
97
|
+
timestamps.length <= 8,
|
|
98
|
+
"missing signing timestamp"
|
|
99
|
+
);
|
|
100
|
+
for (const time of timestamps) {
|
|
101
|
+
const milliseconds = Date.parse(time.timestamp);
|
|
102
|
+
requireProof(
|
|
103
|
+
["Tlog", "TimestampAuthority"].includes(time.type),
|
|
104
|
+
"unsigned/current signing time"
|
|
105
|
+
);
|
|
106
|
+
requireProof(
|
|
107
|
+
typeof time.uri === "string" && time.uri.startsWith("https://"),
|
|
108
|
+
"missing timestamp authority"
|
|
109
|
+
);
|
|
110
|
+
requireProof(
|
|
111
|
+
Number.isFinite(milliseconds) && milliseconds <= now + 60_000,
|
|
112
|
+
"future/invalid signing time"
|
|
113
|
+
);
|
|
114
|
+
requireProof(
|
|
115
|
+
now - milliseconds <= policy.maxAgeSeconds * 1000,
|
|
116
|
+
"proposal authorization expired"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Fixed role matching never accepts a caller-selected expiry or predicate waiver. */
|
|
122
|
+
export function verifiedRole(
|
|
123
|
+
results,
|
|
124
|
+
descriptor,
|
|
125
|
+
digest,
|
|
126
|
+
policy,
|
|
127
|
+
predicate,
|
|
128
|
+
name
|
|
129
|
+
) {
|
|
130
|
+
requireProof(
|
|
131
|
+
Array.isArray(results) && results.length === 1,
|
|
132
|
+
"ambiguous/missing attestation"
|
|
133
|
+
);
|
|
134
|
+
const result = results[0].verificationResult;
|
|
135
|
+
requireProof(
|
|
136
|
+
result?.statement?._type === "https://in-toto.io/Statement/v1",
|
|
137
|
+
"unsupported statement"
|
|
138
|
+
);
|
|
139
|
+
requireProof(
|
|
140
|
+
result.statement.predicateType === predicate,
|
|
141
|
+
"wrong predicate type"
|
|
142
|
+
);
|
|
143
|
+
const subjects = result.statement.subject;
|
|
144
|
+
requireProof(
|
|
145
|
+
Array.isArray(subjects) && subjects.length === 1,
|
|
146
|
+
"ambiguous subject"
|
|
147
|
+
);
|
|
148
|
+
requireProof(
|
|
149
|
+
subjects[0].name === name && subjects[0].digest?.sha256 === digest,
|
|
150
|
+
"proof subject differs"
|
|
151
|
+
);
|
|
152
|
+
matchCertificate(result.signature?.certificate, descriptor, policy);
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Ordinary v1 remains short-lived current authorization. */
|
|
157
|
+
export function assertVerifiedAttestation(
|
|
158
|
+
results,
|
|
159
|
+
descriptor,
|
|
160
|
+
digest,
|
|
161
|
+
policy,
|
|
162
|
+
now = Date.now()
|
|
163
|
+
) {
|
|
164
|
+
const result = verifiedRole(
|
|
165
|
+
results,
|
|
166
|
+
descriptor,
|
|
167
|
+
digest,
|
|
168
|
+
policy,
|
|
169
|
+
PROPOSAL_PREDICATE,
|
|
170
|
+
"descriptor.json"
|
|
171
|
+
);
|
|
172
|
+
matchSigningTime(result.verifiedTimestamps, policy, now);
|
|
173
|
+
return result.signature.certificate;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Shared literal argv does not replace official verification or authorization policy. */
|
|
177
|
+
export function officialArguments(policy, identity, file, bundle, predicate) {
|
|
178
|
+
return [
|
|
179
|
+
"attestation",
|
|
180
|
+
"verify",
|
|
181
|
+
file,
|
|
182
|
+
"--bundle",
|
|
183
|
+
bundle,
|
|
184
|
+
"--repo",
|
|
185
|
+
policy.repository,
|
|
186
|
+
"--signer-workflow",
|
|
187
|
+
policy.signerWorkflow,
|
|
188
|
+
"--signer-digest",
|
|
189
|
+
policy.signerDigest,
|
|
190
|
+
"--source-digest",
|
|
191
|
+
identity.parent,
|
|
192
|
+
"--source-ref",
|
|
193
|
+
"refs/heads/main",
|
|
194
|
+
"--predicate-type",
|
|
195
|
+
predicate,
|
|
196
|
+
"--cert-oidc-issuer",
|
|
197
|
+
ACTIONS_ISSUER,
|
|
198
|
+
"--deny-self-hosted-runners",
|
|
199
|
+
"--format",
|
|
200
|
+
"json",
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The three explicit entry points select fixed official verification roles. */
|
|
205
|
+
export function officialResult(
|
|
206
|
+
policy,
|
|
207
|
+
identity,
|
|
208
|
+
file,
|
|
209
|
+
bundle,
|
|
210
|
+
predicate,
|
|
211
|
+
execute
|
|
212
|
+
) {
|
|
213
|
+
return ghJson(
|
|
214
|
+
policy,
|
|
215
|
+
officialArguments(policy, identity, file, bundle, predicate),
|
|
216
|
+
execute
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Invoke official signature/root/timestamp verification before applying policy. */
|
|
221
|
+
export function verifyDescriptorAttestation(
|
|
222
|
+
policy,
|
|
223
|
+
descriptor,
|
|
224
|
+
paths,
|
|
225
|
+
digest,
|
|
226
|
+
execute = boundedSpawnSync
|
|
227
|
+
) {
|
|
228
|
+
return assertVerifiedAttestation(
|
|
229
|
+
officialResult(
|
|
230
|
+
policy,
|
|
231
|
+
descriptor,
|
|
232
|
+
paths.descriptor,
|
|
233
|
+
paths.bundle,
|
|
234
|
+
PROPOSAL_PREDICATE,
|
|
235
|
+
execute
|
|
236
|
+
),
|
|
237
|
+
descriptor,
|
|
238
|
+
digest,
|
|
239
|
+
policy
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Compatibility exports retain ordinary callers without top-level provider work.
|
|
244
|
+
export { verifyCurrentProvider } from "./github-attestation-provider.mjs";
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
/** Bounded lossless evidence syntax; no digest is authenticated by syntax alone. */
|
|
4
|
+
const credentialField =
|
|
5
|
+
/^(?:api[_-]?key|access[_-]?token|password|passwd|secret)$/iu;
|
|
6
|
+
const credentialToken =
|
|
7
|
+
/^(?:client[_-]?secret|auth[_-]?token|token|credentials?)$/iu;
|
|
8
|
+
export const safeSourcePath = value =>
|
|
9
|
+
typeof value === "string" &&
|
|
10
|
+
value.length > 0 &&
|
|
11
|
+
!credentialField.test(value) &&
|
|
12
|
+
!credentialToken.test(value) &&
|
|
13
|
+
!/[\\\u0000-\u001f\u007f]/u.test(value) &&
|
|
14
|
+
!/^(?:\/|[A-Za-z]:)/u.test(value) &&
|
|
15
|
+
value.split("/").every(part => part && part !== "." && part !== "..");
|
|
16
|
+
const proofLabel = value =>
|
|
17
|
+
safeSourcePath(value) ||
|
|
18
|
+
(typeof value === "string" &&
|
|
19
|
+
value.startsWith("/") &&
|
|
20
|
+
safeSourcePath(value.slice(1)));
|
|
21
|
+
const sources = new Set([
|
|
22
|
+
"source_sha256",
|
|
23
|
+
"source_hashes",
|
|
24
|
+
"source_hashes_after",
|
|
25
|
+
"current_source_sha256",
|
|
26
|
+
"prior66_source_sha256",
|
|
27
|
+
"protected_sha256",
|
|
28
|
+
"selected_artifact_hashes",
|
|
29
|
+
"files",
|
|
30
|
+
]);
|
|
31
|
+
const proofs = new Set(["proof_sha256", "logs"]);
|
|
32
|
+
const isHash = value =>
|
|
33
|
+
typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
|
|
34
|
+
|
|
35
|
+
/** Native validation and duplicate-aware lexical spans share the exact same text. */
|
|
36
|
+
const lexical = text => {
|
|
37
|
+
const value = JSON.parse(text);
|
|
38
|
+
const tokens = [
|
|
39
|
+
...text.matchAll(/"(?:\\[\s\S]|[^"\\])*"|[{}[\],:]|[^{}[\],:\s]+/gu),
|
|
40
|
+
];
|
|
41
|
+
if (tokens.length > 8192) throw new Error("Bounded JSON tokens");
|
|
42
|
+
const state = { cursor: 0, spans: [] };
|
|
43
|
+
visit(tokens, state, [], 0, text);
|
|
44
|
+
if (state.cursor !== tokens.length) throw new Error("Trailing JSON tokens");
|
|
45
|
+
return { value, spans: state.spans };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Duplicate decoded keys and escaped attribution spellings always fail closed. */
|
|
49
|
+
const visit = (tokens, state, path, depth, text) => {
|
|
50
|
+
if (depth > 32) throw new Error("Bounded JSON depth");
|
|
51
|
+
const token = tokens[state.cursor++];
|
|
52
|
+
if (token[0] === "{") {
|
|
53
|
+
const keys = new Set();
|
|
54
|
+
while (tokens[state.cursor][0] !== "}") {
|
|
55
|
+
const keyToken = tokens[state.cursor++][0];
|
|
56
|
+
const key = JSON.parse(keyToken);
|
|
57
|
+
if (keys.has(key) || keyToken !== JSON.stringify(key))
|
|
58
|
+
throw new Error("Ambiguous JSON key");
|
|
59
|
+
keys.add(key);
|
|
60
|
+
state.cursor++;
|
|
61
|
+
visit(tokens, state, [...path, key], depth + 1, text);
|
|
62
|
+
if (tokens[state.cursor][0] === ",") state.cursor++;
|
|
63
|
+
}
|
|
64
|
+
state.cursor++;
|
|
65
|
+
} else if (token[0] === "[") {
|
|
66
|
+
let index = 0;
|
|
67
|
+
while (tokens[state.cursor][0] !== "]") {
|
|
68
|
+
visit(tokens, state, [...path, index++], depth + 1, text);
|
|
69
|
+
if (tokens[state.cursor][0] === ",") state.cursor++;
|
|
70
|
+
}
|
|
71
|
+
state.cursor++;
|
|
72
|
+
} else if (token[0][0] === '"') {
|
|
73
|
+
const hash = JSON.parse(token[0]);
|
|
74
|
+
if (!isHash(hash)) return;
|
|
75
|
+
if (token[0] !== JSON.stringify(hash))
|
|
76
|
+
throw new Error("Ambiguous digest spelling");
|
|
77
|
+
state.spans.push({
|
|
78
|
+
path,
|
|
79
|
+
hash,
|
|
80
|
+
start: Buffer.byteLength(text.slice(0, token.index + 1)),
|
|
81
|
+
end: Buffer.byteLength(text.slice(0, token.index + token[0].length - 1)),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Source maps keep whole-map verification; proof entries attest only themselves. */
|
|
87
|
+
const maps = (text, offset) => {
|
|
88
|
+
const parsed = lexical(text);
|
|
89
|
+
if (
|
|
90
|
+
!parsed.value ||
|
|
91
|
+
typeof parsed.value !== "object" ||
|
|
92
|
+
Array.isArray(parsed.value)
|
|
93
|
+
)
|
|
94
|
+
return [];
|
|
95
|
+
const bare = Object.entries(parsed.value).every(
|
|
96
|
+
([file, hash]) => safeSourcePath(file) && isHash(hash)
|
|
97
|
+
);
|
|
98
|
+
const roles = bare
|
|
99
|
+
? [[null, parsed.value]]
|
|
100
|
+
: Object.entries(parsed.value).filter(
|
|
101
|
+
([role]) => sources.has(role) || proofs.has(role)
|
|
102
|
+
);
|
|
103
|
+
const result = [];
|
|
104
|
+
for (const [role, fields] of roles) {
|
|
105
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields))
|
|
106
|
+
return [];
|
|
107
|
+
const kind = proofs.has(role)
|
|
108
|
+
? "proof"
|
|
109
|
+
: role === "source_sha256"
|
|
110
|
+
? "strict"
|
|
111
|
+
: "source";
|
|
112
|
+
const entries = Object.entries(fields);
|
|
113
|
+
const validPath = kind === "proof" ? proofLabel : safeSourcePath;
|
|
114
|
+
if (
|
|
115
|
+
!entries.length ||
|
|
116
|
+
entries.length > 256 ||
|
|
117
|
+
entries.some(([file, hash]) => !validPath(file) || !isHash(hash))
|
|
118
|
+
)
|
|
119
|
+
return [];
|
|
120
|
+
const spans = parsed.spans
|
|
121
|
+
.filter(span =>
|
|
122
|
+
role === null
|
|
123
|
+
? span.path.length === 1
|
|
124
|
+
: span.path.length === 2 && span.path[0] === role
|
|
125
|
+
)
|
|
126
|
+
.map(span => ({
|
|
127
|
+
file: span.path.at(-1),
|
|
128
|
+
hash: span.hash,
|
|
129
|
+
start: span.start + offset,
|
|
130
|
+
end: span.end + offset,
|
|
131
|
+
}));
|
|
132
|
+
if (spans.length !== entries.length) return [];
|
|
133
|
+
result.push({ kind, value: parsed.value, spans });
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** Fenced JSON keeps original whole-file byte offsets for actual detector attribution. */
|
|
139
|
+
export const evidenceMaps = bytes => {
|
|
140
|
+
if (bytes.length > 1024 * 1024) return [];
|
|
141
|
+
const text = bytes.toString("utf8");
|
|
142
|
+
if (!Buffer.from(text).equals(bytes)) return [];
|
|
143
|
+
try {
|
|
144
|
+
return maps(text, 0);
|
|
145
|
+
} catch {
|
|
146
|
+
/* Ordinary Markdown can contain exact JSON fences. */
|
|
147
|
+
}
|
|
148
|
+
const result = [];
|
|
149
|
+
for (const block of text.matchAll(
|
|
150
|
+
/(?:^|\n)```json\n([\s\S]*?)\n```(?=\n|$)/gu
|
|
151
|
+
)) {
|
|
152
|
+
const offset = Buffer.byteLength(
|
|
153
|
+
text.slice(0, block.index + block[0].indexOf(block[1]))
|
|
154
|
+
);
|
|
155
|
+
try {
|
|
156
|
+
result.push(...maps(block[1], offset));
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** Only the fixed taxonomy in ordinary prose, outside Markdown code, is a candidate. */
|
|
165
|
+
export const narrativeSpan = (bytes, lineStart, lineEnd) => {
|
|
166
|
+
const prior = bytes.subarray(0, lineStart).toString("utf8");
|
|
167
|
+
const fences = prior.split(/[\r\n\u2028\u2029]/u).filter(part => {
|
|
168
|
+
const trimmed = part.trimStart();
|
|
169
|
+
return trimmed.startsWith("```") || trimmed.startsWith("~~~");
|
|
170
|
+
});
|
|
171
|
+
if (fences.length % 2) return null;
|
|
172
|
+
const line = bytes.subarray(lineStart, lineEnd).toString("utf8");
|
|
173
|
+
const prose = line.replace(/`[^`\r\n]{1,256}`/gu, "identifier");
|
|
174
|
+
if (!/^(?:[-*] )?[A-Za-z][A-Za-z0-9 ,./():-]*\.$/u.test(prose)) return null;
|
|
175
|
+
const taxonomy = "Disk/missing/corrupt";
|
|
176
|
+
const phrase = `AccessDenied boundaries, ${taxonomy} falsifications`;
|
|
177
|
+
const index = line.indexOf(phrase);
|
|
178
|
+
if (index < 0 || line.indexOf(phrase, index + 1) >= 0) return null;
|
|
179
|
+
if (
|
|
180
|
+
!/[,.]/u.test(line[index + phrase.length] ?? "") ||
|
|
181
|
+
line.slice(0, index).split("`").length % 2 === 0
|
|
182
|
+
)
|
|
183
|
+
return null;
|
|
184
|
+
const start =
|
|
185
|
+
lineStart +
|
|
186
|
+
Buffer.byteLength(line.slice(0, index + phrase.indexOf(taxonomy)));
|
|
187
|
+
return { hash: taxonomy, start, end: start + taxonomy.length };
|
|
188
|
+
};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
/** Exact Git preimages and detector spans, independent of caller suppressions. */
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import {
|
|
7
|
+
gitEnvironment,
|
|
8
|
+
gitRead,
|
|
9
|
+
HistorySecretError,
|
|
10
|
+
} from "./history-secret-git.mjs";
|
|
11
|
+
import {
|
|
12
|
+
evidenceMaps,
|
|
13
|
+
narrativeSpan,
|
|
14
|
+
safeSourcePath,
|
|
15
|
+
} from "./history-secret-evidence-shape.mjs";
|
|
16
|
+
const BUDGET = 64 * 1024 * 1024;
|
|
17
|
+
const NO_REPLACEMENTS = "--no-replace-objects";
|
|
18
|
+
const digest = bytes => createHash("sha256").update(bytes).digest("hex");
|
|
19
|
+
const options = cwd => ({
|
|
20
|
+
cwd,
|
|
21
|
+
env: gitEnvironment(),
|
|
22
|
+
timeout: 120000,
|
|
23
|
+
maxBuffer: 1024 * 1024,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** Authentic regular Git blobs; deduplication charges each actual blob once. */
|
|
27
|
+
const readBlob = (file, commit, cwd, state) => {
|
|
28
|
+
if (!safeSourcePath(file)) return null;
|
|
29
|
+
const entries = gitRead(
|
|
30
|
+
["--literal-pathspecs", "ls-tree", "-z", commit, "--", file],
|
|
31
|
+
cwd
|
|
32
|
+
)
|
|
33
|
+
.split("\0")
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
if (entries.length !== 1) return null;
|
|
36
|
+
const entry = entries[0].match(
|
|
37
|
+
/^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t([\s\S]*)$/u
|
|
38
|
+
);
|
|
39
|
+
if (!entry || entry[3] !== file || entry[2].length !== commit.length)
|
|
40
|
+
return null;
|
|
41
|
+
if (state.blobs.has(entry[2])) return state.blobs.get(entry[2]);
|
|
42
|
+
if (state.blobs.size >= 256) return null;
|
|
43
|
+
const sizeText = gitRead(["cat-file", "-s", entry[2]], cwd);
|
|
44
|
+
const size = Number(sizeText);
|
|
45
|
+
if (!/^\d+$/u.test(sizeText) || !Number.isSafeInteger(size))
|
|
46
|
+
throw new HistorySecretError(
|
|
47
|
+
"Git blob size is invalid. Repair complete Git objects and retry; safety is unproved."
|
|
48
|
+
);
|
|
49
|
+
if (size > state.remaining) return null;
|
|
50
|
+
const result = spawnSync(
|
|
51
|
+
"git",
|
|
52
|
+
[NO_REPLACEMENTS, "cat-file", "blob", entry[2]],
|
|
53
|
+
{
|
|
54
|
+
cwd,
|
|
55
|
+
env: gitEnvironment(),
|
|
56
|
+
timeout: 120000,
|
|
57
|
+
maxBuffer: BUDGET,
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
if (
|
|
61
|
+
result.error ||
|
|
62
|
+
result.signal ||
|
|
63
|
+
result.status !== 0 ||
|
|
64
|
+
result.stdout.length !== size ||
|
|
65
|
+
createHash(commit.length === 40 ? "sha1" : "sha256")
|
|
66
|
+
.update(`blob ${size}\0`)
|
|
67
|
+
.update(result.stdout)
|
|
68
|
+
.digest("hex") !== entry[2]
|
|
69
|
+
)
|
|
70
|
+
throw new HistorySecretError(
|
|
71
|
+
"Git blob bytes cannot be verified. Repair complete Git objects and retry; no raw source is published."
|
|
72
|
+
);
|
|
73
|
+
state.remaining -= size;
|
|
74
|
+
state.blobs.set(entry[2], result.stdout);
|
|
75
|
+
return result.stdout;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Explicit revisions retain exact original commit-type and ancestry requirements. */
|
|
79
|
+
const revisionFor = (map, row, cwd) => {
|
|
80
|
+
if (!Object.hasOwn(map.value, "source_revision")) return row.Commit;
|
|
81
|
+
const revision = map.value.source_revision;
|
|
82
|
+
if (
|
|
83
|
+
typeof revision !== "string" ||
|
|
84
|
+
!new RegExp(`^[a-f0-9]{${row.Commit.length}}$`, "u").test(revision)
|
|
85
|
+
)
|
|
86
|
+
return null;
|
|
87
|
+
const type = spawnSync(
|
|
88
|
+
"git",
|
|
89
|
+
[NO_REPLACEMENTS, "cat-file", "-t", revision],
|
|
90
|
+
options(cwd)
|
|
91
|
+
);
|
|
92
|
+
const ancestor = spawnSync(
|
|
93
|
+
"git",
|
|
94
|
+
[NO_REPLACEMENTS, "merge-base", "--is-ancestor", revision, row.Commit],
|
|
95
|
+
options(cwd)
|
|
96
|
+
);
|
|
97
|
+
return !type.error &&
|
|
98
|
+
!type.signal &&
|
|
99
|
+
type.status === 0 &&
|
|
100
|
+
type.stdout.toString() === "commit\n" &&
|
|
101
|
+
!type.stderr.length &&
|
|
102
|
+
!ancestor.error &&
|
|
103
|
+
!ancestor.signal &&
|
|
104
|
+
ancestor.status === 0 &&
|
|
105
|
+
!ancestor.stdout.length &&
|
|
106
|
+
!ancestor.stderr.length
|
|
107
|
+
? revision
|
|
108
|
+
: null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Only explicitly selected introduced commits can supply additional source versions. */
|
|
112
|
+
const sourceVersions = (file, row, cwd, commits, cache) => {
|
|
113
|
+
const key = `${row.Commit}\0${file}`;
|
|
114
|
+
if (cache.has(key)) return cache.get(key);
|
|
115
|
+
const candidates = new Set();
|
|
116
|
+
for (let offset = 0; offset < commits.length; offset += 100) {
|
|
117
|
+
const list = gitRead(
|
|
118
|
+
[
|
|
119
|
+
"--literal-pathspecs",
|
|
120
|
+
"log",
|
|
121
|
+
"--no-walk=unsorted",
|
|
122
|
+
"--format=%H",
|
|
123
|
+
...commits.slice(offset, offset + 100),
|
|
124
|
+
"--",
|
|
125
|
+
file,
|
|
126
|
+
],
|
|
127
|
+
cwd
|
|
128
|
+
);
|
|
129
|
+
for (const candidate of list.split("\n").filter(Boolean)) {
|
|
130
|
+
if (!commits.includes(candidate))
|
|
131
|
+
throw new HistorySecretError(
|
|
132
|
+
"Source version attribution is invalid. Supply original Git input and retry."
|
|
133
|
+
);
|
|
134
|
+
const ancestor = spawnSync(
|
|
135
|
+
"git",
|
|
136
|
+
[NO_REPLACEMENTS, "merge-base", "--is-ancestor", candidate, row.Commit],
|
|
137
|
+
options(cwd)
|
|
138
|
+
);
|
|
139
|
+
if (
|
|
140
|
+
ancestor.error ||
|
|
141
|
+
ancestor.signal ||
|
|
142
|
+
![0, 1].includes(ancestor.status) ||
|
|
143
|
+
ancestor.stdout.length ||
|
|
144
|
+
ancestor.stderr.length
|
|
145
|
+
)
|
|
146
|
+
throw new HistorySecretError(
|
|
147
|
+
"Source ancestry cannot be verified. Repair complete Git objects and retry."
|
|
148
|
+
);
|
|
149
|
+
if (ancestor.status === 0) candidates.add(candidate);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const versions = [...candidates];
|
|
153
|
+
cache.set(key, versions);
|
|
154
|
+
return versions;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** One reconstructed redacted capture must coincide with one actual Git byte span. */
|
|
158
|
+
const attribution = (row, bytes) => {
|
|
159
|
+
let lineStart = 0;
|
|
160
|
+
for (let line = 1; line < row.StartLine; line++) {
|
|
161
|
+
const next = bytes.indexOf(10, lineStart);
|
|
162
|
+
if (next < 0) return null;
|
|
163
|
+
lineStart = next + 1;
|
|
164
|
+
}
|
|
165
|
+
const newline = bytes.indexOf(10, lineStart);
|
|
166
|
+
const lineEnd = newline < 0 ? bytes.length : newline;
|
|
167
|
+
const replacements = row.Match.split("REDACTED");
|
|
168
|
+
if (replacements.length !== 2) return null;
|
|
169
|
+
const line = bytes.subarray(lineStart, lineEnd);
|
|
170
|
+
const matches = span => {
|
|
171
|
+
const template = replacements.join(span.hash);
|
|
172
|
+
if (template.split(span.hash).length !== 2) return false;
|
|
173
|
+
const original = Buffer.from(template);
|
|
174
|
+
const offset = line.indexOf(original);
|
|
175
|
+
if (offset < 0 || line.indexOf(original, offset + 1) >= 0) return false;
|
|
176
|
+
const start = lineStart + offset;
|
|
177
|
+
const end = start + original.length;
|
|
178
|
+
if (
|
|
179
|
+
span.start !== start + Buffer.byteLength(replacements[0]) ||
|
|
180
|
+
span.end !== span.start + Buffer.byteLength(span.hash)
|
|
181
|
+
)
|
|
182
|
+
return false;
|
|
183
|
+
// The pinned vendor can count one preceding fragment newline in columns.
|
|
184
|
+
return (
|
|
185
|
+
[0, 1].filter(
|
|
186
|
+
extra =>
|
|
187
|
+
start === lineStart + row.StartColumn - 1 - extra &&
|
|
188
|
+
end === lineStart + row.EndColumn - extra
|
|
189
|
+
).length === 1
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
return { lineStart, lineEnd, matches };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** Catalogue labels are never dereferenced; only actual introduced head Git bytes count. */
|
|
196
|
+
const proofMatches = (span, heads, blob) =>
|
|
197
|
+
heads.some(head => {
|
|
198
|
+
const bytes = blob(
|
|
199
|
+
`.lisa/history-secret-preimages/sha256/${span.hash}`,
|
|
200
|
+
head
|
|
201
|
+
);
|
|
202
|
+
return bytes !== null && digest(bytes) === span.hash;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/** Source mappings retain whole-map verification and exact path-specific preimages. */
|
|
206
|
+
const verifiedMap = (map, row, cwd, commits, heads, blob, versions) => {
|
|
207
|
+
if (map.kind === "proof")
|
|
208
|
+
return map.spans.filter(span => proofMatches(span, heads, blob));
|
|
209
|
+
const revision = revisionFor(map, row, cwd);
|
|
210
|
+
if (revision === null) return [];
|
|
211
|
+
for (const span of map.spans) {
|
|
212
|
+
const bytes = blob(span.file, revision);
|
|
213
|
+
if (bytes !== null && digest(bytes) === span.hash) continue;
|
|
214
|
+
if (map.kind === "strict" || Object.hasOwn(map.value, "source_revision"))
|
|
215
|
+
return [];
|
|
216
|
+
if (
|
|
217
|
+
!sourceVersions(span.file, row, cwd, commits, versions).some(commit => {
|
|
218
|
+
const historical = blob(span.file, commit);
|
|
219
|
+
return historical !== null && digest(historical) === span.hash;
|
|
220
|
+
})
|
|
221
|
+
)
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
return map.spans;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Clear a finding only after syntax, original span and independently verified bytes agree. */
|
|
228
|
+
export const classifyEvidence = (cwd, commits, heads) => {
|
|
229
|
+
const state = { remaining: BUDGET, blobs: new Map() };
|
|
230
|
+
const refs = new Map();
|
|
231
|
+
const maps = new Map();
|
|
232
|
+
const versions = new Map();
|
|
233
|
+
const blob = (file, commit) => {
|
|
234
|
+
const key = `${commit}\0${file}`;
|
|
235
|
+
if (!refs.has(key)) {
|
|
236
|
+
if (refs.size >= 4096) return null;
|
|
237
|
+
refs.set(key, readBlob(file, commit, cwd, state));
|
|
238
|
+
}
|
|
239
|
+
return refs.get(key);
|
|
240
|
+
};
|
|
241
|
+
return row => {
|
|
242
|
+
if (
|
|
243
|
+
row.RuleID !== "generic-api-key" ||
|
|
244
|
+
!safeSourcePath(row.File) ||
|
|
245
|
+
row.SymlinkFile !== "" ||
|
|
246
|
+
row.Secret !== "REDACTED" ||
|
|
247
|
+
typeof row.Match !== "string" ||
|
|
248
|
+
row.EndLine !== row.StartLine ||
|
|
249
|
+
!Number.isSafeInteger(row.StartColumn) ||
|
|
250
|
+
!Number.isSafeInteger(row.EndColumn) ||
|
|
251
|
+
row.StartColumn < 1 ||
|
|
252
|
+
row.EndColumn < row.StartColumn
|
|
253
|
+
)
|
|
254
|
+
return false;
|
|
255
|
+
const bytes = blob(row.File, row.Commit);
|
|
256
|
+
if (!bytes) return false;
|
|
257
|
+
const span = attribution(row, bytes);
|
|
258
|
+
if (!span) return false;
|
|
259
|
+
const narrative = narrativeSpan(bytes, span.lineStart, span.lineEnd);
|
|
260
|
+
if (narrative !== null && span.matches(narrative)) return true;
|
|
261
|
+
const key = `${row.Commit}\0${row.File}`;
|
|
262
|
+
if (!maps.has(key))
|
|
263
|
+
maps.set(
|
|
264
|
+
key,
|
|
265
|
+
evidenceMaps(bytes).flatMap(map =>
|
|
266
|
+
verifiedMap(map, row, cwd, commits, heads, blob, versions)
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
return maps.get(key).filter(span.matches).length === 1;
|
|
270
|
+
};
|
|
271
|
+
};
|