@xagents_in/unmask 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +128 -2
- package/dist/cli/hosted.mjs +251 -0
- package/dist/cli/main.mjs +210 -0
- package/dist/core/acquire.mjs +283 -0
- package/dist/core/archive.mjs +485 -0
- package/dist/core/compare.mjs +230 -0
- package/dist/core/manifests.mjs +548 -0
- package/dist/core/online.mjs +310 -0
- package/dist/core/pipeline.mjs +345 -0
- package/dist/core/policy.mjs +92 -0
- package/dist/core/report.mjs +506 -0
- package/dist/core/resolve.mjs +186 -0
- package/dist/core/source.mjs +106 -0
- package/dist/core/syntax.mjs +340 -0
- package/dist/model/client.mjs +1257 -0
- package/dist/model/config.mjs +90 -0
- package/dist/server/app.mjs +299 -0
- package/dist/server/main.mjs +146 -0
- package/dist/server/queue.mjs +177 -0
- package/dist/server/runner.mjs +280 -0
- package/dist/server/store.mjs +105 -0
- package/dist/server/worker.mjs +57 -0
- package/dist/version.json +1 -0
- package/dist/viewer/render.mjs +255 -0
- package/dist/viewer/shell.mjs +73 -0
- package/package.json +39 -4
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
function repository(value) {
|
|
2
|
+
const input = typeof value === "string" ? value : value?.url;
|
|
3
|
+
if (
|
|
4
|
+
typeof input !== "string" ||
|
|
5
|
+
input.length > 512 ||
|
|
6
|
+
/[\s%\\?#\0]/u.test(input)
|
|
7
|
+
)
|
|
8
|
+
return null;
|
|
9
|
+
let pair;
|
|
10
|
+
if (input.startsWith("github:")) pair = input.slice(7);
|
|
11
|
+
else if (input.startsWith("git@github.com:"))
|
|
12
|
+
pair = input.slice("git@github.com:".length);
|
|
13
|
+
else {
|
|
14
|
+
const match =
|
|
15
|
+
/^(https|git\+https|git|ssh|git\+ssh):\/\/([^/]+)\/(.+)$/u.exec(input);
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
const ssh = ["ssh", "git+ssh"].includes(match[1]);
|
|
18
|
+
if (match[2] !== (ssh ? "git@github.com" : "github.com")) return null;
|
|
19
|
+
pair = match[3];
|
|
20
|
+
}
|
|
21
|
+
if (pair.endsWith(".git")) pair = pair.slice(0, -4);
|
|
22
|
+
const match =
|
|
23
|
+
/^([a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?)\/([a-z0-9_.-]{1,100})$/iu.exec(
|
|
24
|
+
pair,
|
|
25
|
+
);
|
|
26
|
+
if (!match || match[1].includes("--") || [".", ".."].includes(match[2]))
|
|
27
|
+
return null;
|
|
28
|
+
return {
|
|
29
|
+
owner: match[1].toLowerCase(),
|
|
30
|
+
repo: match[2].toLowerCase(),
|
|
31
|
+
repository: `https://github.com/${match[1].toLowerCase()}/${match[2].toLowerCase()}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function directory(value) {
|
|
35
|
+
if (value === undefined || value === "") return "";
|
|
36
|
+
if (
|
|
37
|
+
typeof value !== "string" ||
|
|
38
|
+
value.length > 4096 ||
|
|
39
|
+
/[\0\\]/u.test(value) ||
|
|
40
|
+
value.startsWith("/") ||
|
|
41
|
+
/^[a-z]:/iu.test(value) ||
|
|
42
|
+
value.split("/").some((p) => !p || p === "." || p === "..")
|
|
43
|
+
)
|
|
44
|
+
return null;
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function candidate(repo, commit, dir, method) {
|
|
48
|
+
if (
|
|
49
|
+
!repo ||
|
|
50
|
+
typeof commit !== "string" ||
|
|
51
|
+
!/^[a-f0-9]{40}$/iu.test(commit) ||
|
|
52
|
+
dir === null
|
|
53
|
+
)
|
|
54
|
+
return null;
|
|
55
|
+
const sha = commit.toLowerCase();
|
|
56
|
+
return {
|
|
57
|
+
repository: repo.repository,
|
|
58
|
+
commit: sha,
|
|
59
|
+
directory: dir,
|
|
60
|
+
method,
|
|
61
|
+
archiveUrl: `https://codeload.github.com/${repo.owner}/${repo.repo}/tar.gz/${sha}`,
|
|
62
|
+
sourceRoot: `${repo.repo}-${sha}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function same(a, b) {
|
|
66
|
+
return a.repository === b.repository && a.commit === b.commit;
|
|
67
|
+
}
|
|
68
|
+
function identity(value) {
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
const at = value.lastIndexOf("@");
|
|
71
|
+
return at > 0
|
|
72
|
+
? { name: value.slice(0, at), version: value.slice(at + 1) }
|
|
73
|
+
: null;
|
|
74
|
+
}
|
|
75
|
+
return value &&
|
|
76
|
+
typeof value.name === "string" &&
|
|
77
|
+
typeof value.version === "string"
|
|
78
|
+
? { name: value.name, version: value.version }
|
|
79
|
+
: null;
|
|
80
|
+
}
|
|
81
|
+
export function resolveSource({
|
|
82
|
+
claim,
|
|
83
|
+
artifactManifest,
|
|
84
|
+
registryDoc,
|
|
85
|
+
explicitSource,
|
|
86
|
+
commit,
|
|
87
|
+
sourceDir,
|
|
88
|
+
} = {}) {
|
|
89
|
+
const result = {
|
|
90
|
+
status: "unresolved",
|
|
91
|
+
method: "unresolved",
|
|
92
|
+
candidate: null,
|
|
93
|
+
alternatives: [],
|
|
94
|
+
provenance: { status: "not-assessed" },
|
|
95
|
+
conflicts: [],
|
|
96
|
+
};
|
|
97
|
+
const requested = identity(claim),
|
|
98
|
+
artifact = identity(artifactManifest),
|
|
99
|
+
registry = identity(registryDoc);
|
|
100
|
+
if (
|
|
101
|
+
(requested &&
|
|
102
|
+
artifact &&
|
|
103
|
+
(requested.name !== artifact.name ||
|
|
104
|
+
requested.version !== artifact.version)) ||
|
|
105
|
+
(requested &&
|
|
106
|
+
registry &&
|
|
107
|
+
(requested.name !== registry.name ||
|
|
108
|
+
requested.version !== registry.version))
|
|
109
|
+
)
|
|
110
|
+
result.conflicts.push({
|
|
111
|
+
code: "identity-claim-conflict",
|
|
112
|
+
claim: requested,
|
|
113
|
+
artifact,
|
|
114
|
+
registry,
|
|
115
|
+
});
|
|
116
|
+
const registryRepo = repository(registryDoc?.repository);
|
|
117
|
+
const artifactRepo = repository(artifactManifest?.repository);
|
|
118
|
+
if (
|
|
119
|
+
registryRepo &&
|
|
120
|
+
artifactRepo &&
|
|
121
|
+
registryRepo.repository !== artifactRepo.repository
|
|
122
|
+
)
|
|
123
|
+
result.conflicts.push({
|
|
124
|
+
code: "repository-claim-conflict",
|
|
125
|
+
artifact: artifactRepo.repository,
|
|
126
|
+
registry: registryRepo.repository,
|
|
127
|
+
});
|
|
128
|
+
const publisher = candidate(
|
|
129
|
+
registryRepo,
|
|
130
|
+
registryDoc?.gitHead,
|
|
131
|
+
directory(registryDoc?.repository?.directory),
|
|
132
|
+
"publisher-gitHead",
|
|
133
|
+
);
|
|
134
|
+
if (publisher) result.alternatives.push(publisher);
|
|
135
|
+
const explicitRepo =
|
|
136
|
+
typeof explicitSource === "string" ? repository(explicitSource) : null;
|
|
137
|
+
if (explicitSource !== undefined && !explicitRepo) {
|
|
138
|
+
result.reason = "unsupported-explicit-repository";
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
const dir = sourceDir === undefined ? undefined : directory(sourceDir);
|
|
142
|
+
if (dir === null) {
|
|
143
|
+
result.reason = "invalid-source-directory";
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
if (commit !== undefined) {
|
|
147
|
+
const pinned = candidate(
|
|
148
|
+
explicitRepo ?? registryRepo ?? artifactRepo,
|
|
149
|
+
commit,
|
|
150
|
+
dir ?? "",
|
|
151
|
+
"user-full-SHA",
|
|
152
|
+
);
|
|
153
|
+
if (!pinned) {
|
|
154
|
+
result.reason = "invalid-user-pin";
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
if (publisher && !same(publisher, pinned))
|
|
158
|
+
result.conflicts.push({
|
|
159
|
+
code: "source-claim-conflict",
|
|
160
|
+
alternatives: [publisher, pinned],
|
|
161
|
+
});
|
|
162
|
+
result.alternatives.push(pinned);
|
|
163
|
+
return {
|
|
164
|
+
...result,
|
|
165
|
+
status: "selected",
|
|
166
|
+
method: pinned.method,
|
|
167
|
+
candidate: pinned,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (!publisher) {
|
|
171
|
+
result.reason = "no-full-sha-source-candidate";
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
if (explicitRepo && explicitRepo.repository !== publisher.repository) {
|
|
175
|
+
result.reason = "explicit-repository-disagrees";
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
const selected =
|
|
179
|
+
dir === undefined ? publisher : { ...publisher, directory: dir };
|
|
180
|
+
return {
|
|
181
|
+
...result,
|
|
182
|
+
status: "selected",
|
|
183
|
+
method: selected.method,
|
|
184
|
+
candidate: selected,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
function unresolved(reason, directory = null) {
|
|
2
|
+
return {
|
|
3
|
+
status: "unresolved",
|
|
4
|
+
directory,
|
|
5
|
+
entries: [],
|
|
6
|
+
manifest: null,
|
|
7
|
+
reason,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function directoryPath(value) {
|
|
11
|
+
if (
|
|
12
|
+
typeof value !== "string" ||
|
|
13
|
+
/[\0\\]/u.test(value) ||
|
|
14
|
+
value.startsWith("/") ||
|
|
15
|
+
/^[a-z]:/iu.test(value)
|
|
16
|
+
)
|
|
17
|
+
return null;
|
|
18
|
+
if (value === "") return "";
|
|
19
|
+
const path = value.replace(/\/+$/u, "");
|
|
20
|
+
return path.split("/").some((p) => !p || p === "." || p === "..")
|
|
21
|
+
? null
|
|
22
|
+
: path;
|
|
23
|
+
}
|
|
24
|
+
function manifest(entry) {
|
|
25
|
+
if (entry?.type !== "file" || !Buffer.isBuffer(entry.content)) return null;
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(entry.content.toString("utf8"));
|
|
28
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
29
|
+
? value
|
|
30
|
+
: null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function blocked(entries, directory) {
|
|
36
|
+
return entries.some(
|
|
37
|
+
(e) =>
|
|
38
|
+
e.path !== null &&
|
|
39
|
+
e.path !== undefined &&
|
|
40
|
+
e.type !== "directory" &&
|
|
41
|
+
(e.path === directory || directory.startsWith(e.path + "/")),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
export function selectSourcePackage(
|
|
45
|
+
inventory,
|
|
46
|
+
{ directory, packageName } = {},
|
|
47
|
+
) {
|
|
48
|
+
const entries = inventory.entries;
|
|
49
|
+
let selected = directory;
|
|
50
|
+
if (selected !== undefined) {
|
|
51
|
+
selected = directoryPath(selected);
|
|
52
|
+
if (selected === null) return unresolved("invalid-directory");
|
|
53
|
+
} else {
|
|
54
|
+
if (!inventory.complete) return unresolved("incomplete-inventory");
|
|
55
|
+
const candidates = entries.filter(
|
|
56
|
+
(e) =>
|
|
57
|
+
(e.path === "package.json" || e.path?.endsWith("/package.json")) &&
|
|
58
|
+
typeof packageName === "string" &&
|
|
59
|
+
manifest(e)?.name === packageName,
|
|
60
|
+
);
|
|
61
|
+
if (candidates.length !== 1)
|
|
62
|
+
return unresolved(
|
|
63
|
+
candidates.length ? "ambiguous-manifest" : "manifest-not-found",
|
|
64
|
+
);
|
|
65
|
+
selected = candidates[0].path
|
|
66
|
+
.slice(0, -"package.json".length)
|
|
67
|
+
.replace(/\/$/u, "");
|
|
68
|
+
}
|
|
69
|
+
if (selected && blocked(entries, selected))
|
|
70
|
+
return unresolved("nonregular-package-root", selected);
|
|
71
|
+
const prefix = selected ? selected + "/" : "";
|
|
72
|
+
const selectedEntries = entries
|
|
73
|
+
.filter(
|
|
74
|
+
(e) =>
|
|
75
|
+
typeof e.path === "string" &&
|
|
76
|
+
e.path.startsWith(prefix) &&
|
|
77
|
+
e.path !== selected,
|
|
78
|
+
)
|
|
79
|
+
.map((e) => ({ ...e, path: e.path.slice(prefix.length) }))
|
|
80
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
81
|
+
const existingDirectory = entries.some(
|
|
82
|
+
(e) =>
|
|
83
|
+
e.type === "directory" &&
|
|
84
|
+
(e.path === selected || (selected === "" && e.path === null)),
|
|
85
|
+
);
|
|
86
|
+
if (!selectedEntries.length && !existingDirectory)
|
|
87
|
+
return unresolved("directory-not-found", selected);
|
|
88
|
+
if (selectedEntries.some((e) => directoryPath(e.path) === null))
|
|
89
|
+
return unresolved("unsafe-mapped-path", selected);
|
|
90
|
+
const manifestEntry = selectedEntries.find((e) => e.path === "package.json");
|
|
91
|
+
const parsed = manifest(manifestEntry);
|
|
92
|
+
const reason = !manifestEntry
|
|
93
|
+
? "missing"
|
|
94
|
+
: manifestEntry.type !== "file"
|
|
95
|
+
? "nonregular-manifest"
|
|
96
|
+
: !Buffer.isBuffer(manifestEntry.content)
|
|
97
|
+
? manifestEntry.inspection
|
|
98
|
+
: "invalid-json";
|
|
99
|
+
return {
|
|
100
|
+
status: "selected",
|
|
101
|
+
directory: selected,
|
|
102
|
+
entries: selectedEntries,
|
|
103
|
+
manifest: parsed,
|
|
104
|
+
...(!parsed ? { reason } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { parse } from "acorn";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { validateReport } from "./report.mjs";
|
|
4
|
+
|
|
5
|
+
export const SYNTAX_LIMITS = Object.freeze({
|
|
6
|
+
fileBytes: 2097152,
|
|
7
|
+
totalBytesPerSide: 8388608,
|
|
8
|
+
tokens: 200000,
|
|
9
|
+
cues: 1000,
|
|
10
|
+
sourceLiterals: 10000,
|
|
11
|
+
});
|
|
12
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
13
|
+
function offsets(text) {
|
|
14
|
+
const result = new Uint32Array(text.length + 1);
|
|
15
|
+
let index = 0,
|
|
16
|
+
bytes = 0;
|
|
17
|
+
for (const character of text) {
|
|
18
|
+
result[index] = bytes;
|
|
19
|
+
if (character.length === 2) result[index + 1] = bytes;
|
|
20
|
+
index += character.length;
|
|
21
|
+
bytes += Buffer.byteLength(character);
|
|
22
|
+
result[index] = bytes;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
function evidenceFor(side, inventory, entry, node, positions) {
|
|
27
|
+
const range = [positions[node.start], positions[node.end]];
|
|
28
|
+
const identity = [
|
|
29
|
+
side,
|
|
30
|
+
inventory.digest.sha256,
|
|
31
|
+
entry.path ?? entry.originalPath,
|
|
32
|
+
range,
|
|
33
|
+
];
|
|
34
|
+
const evidence = {
|
|
35
|
+
id: `ev-${hash(JSON.stringify(identity))}`,
|
|
36
|
+
side,
|
|
37
|
+
blobDigest: inventory.digest.sha256,
|
|
38
|
+
path: entry.path,
|
|
39
|
+
originalPath: entry.originalPath,
|
|
40
|
+
fileSha256: entry.sha256,
|
|
41
|
+
size: entry.size,
|
|
42
|
+
range,
|
|
43
|
+
};
|
|
44
|
+
let end = Math.min(range[1], range[0] + 512);
|
|
45
|
+
while (end > range[0]) {
|
|
46
|
+
try {
|
|
47
|
+
evidence.excerpt = new TextDecoder("utf-8", {
|
|
48
|
+
fatal: true,
|
|
49
|
+
ignoreBOM: true,
|
|
50
|
+
}).decode(entry.content.subarray(range[0], end));
|
|
51
|
+
evidence.excerptRange = [range[0], end];
|
|
52
|
+
break;
|
|
53
|
+
} catch {
|
|
54
|
+
end--;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return evidence;
|
|
58
|
+
}
|
|
59
|
+
function nameOf(node) {
|
|
60
|
+
const parts = [];
|
|
61
|
+
for (let depth = 0; node && depth < 4; depth++) {
|
|
62
|
+
if (node.type === "Identifier") return [node.name, ...parts].join(".");
|
|
63
|
+
if (
|
|
64
|
+
node.type === "CallExpression" &&
|
|
65
|
+
node.callee?.type === "Identifier" &&
|
|
66
|
+
node.callee.name === "require" &&
|
|
67
|
+
node.arguments.length === 1 &&
|
|
68
|
+
node.arguments[0].type === "Literal" &&
|
|
69
|
+
typeof node.arguments[0].value === "string"
|
|
70
|
+
) {
|
|
71
|
+
const module = node.arguments[0].value.replace(/^node:/, "");
|
|
72
|
+
return ["child_process", "http", "https", "net"].includes(module)
|
|
73
|
+
? [module, ...parts].join(".")
|
|
74
|
+
: "";
|
|
75
|
+
}
|
|
76
|
+
if (node.type !== "MemberExpression" || node.computed) return "";
|
|
77
|
+
parts.unshift(node.property.name);
|
|
78
|
+
node = node.object;
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
function capability(node) {
|
|
83
|
+
const name = nameOf(node.callee);
|
|
84
|
+
if (
|
|
85
|
+
node.type === "CallExpression" &&
|
|
86
|
+
/^(fetch|globalThis.fetch|https?\.(request|get)|net\.connect)$/.test(name)
|
|
87
|
+
)
|
|
88
|
+
return "network-call";
|
|
89
|
+
if (
|
|
90
|
+
node.type === "CallExpression" &&
|
|
91
|
+
/^(child_process\.)?(exec|execSync|execFile|execFileSync|spawn|spawnSync)$/.test(
|
|
92
|
+
name,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return "process-execution";
|
|
96
|
+
if (
|
|
97
|
+
["CallExpression", "NewExpression"].includes(node.type) &&
|
|
98
|
+
/^(eval|Function)$/.test(name)
|
|
99
|
+
)
|
|
100
|
+
return "dynamic-code";
|
|
101
|
+
if (
|
|
102
|
+
node.type === "MemberExpression" &&
|
|
103
|
+
nameOf(node).startsWith("process.env.")
|
|
104
|
+
)
|
|
105
|
+
return "environment-access";
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function nodes(ast) {
|
|
109
|
+
const stack = [ast],
|
|
110
|
+
result = [];
|
|
111
|
+
while (stack.length) {
|
|
112
|
+
const node = stack.pop();
|
|
113
|
+
result.push(node);
|
|
114
|
+
for (const value of Object.values(node)) {
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
for (let i = value.length - 1; i >= 0; i--)
|
|
117
|
+
if (value[i]?.type) stack.push(value[i]);
|
|
118
|
+
} else if (value?.type) stack.push(value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function inspectJavaScript(artifact, source, overrides = {}) {
|
|
125
|
+
const limits = { ...SYNTAX_LIMITS };
|
|
126
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
127
|
+
if (
|
|
128
|
+
!(key in limits) ||
|
|
129
|
+
!Number.isSafeInteger(value) ||
|
|
130
|
+
value < 0 ||
|
|
131
|
+
value > limits[key]
|
|
132
|
+
)
|
|
133
|
+
throw new TypeError("Syntax limits may only lower defaults");
|
|
134
|
+
limits[key] = value;
|
|
135
|
+
}
|
|
136
|
+
const result = {
|
|
137
|
+
cues: [],
|
|
138
|
+
evidence: [],
|
|
139
|
+
coverage: { files: 0, bytes: 0 },
|
|
140
|
+
sourceCoverage: { files: 0, bytes: 0 },
|
|
141
|
+
annotations: [],
|
|
142
|
+
limits,
|
|
143
|
+
};
|
|
144
|
+
const literals = new Map(),
|
|
145
|
+
candidates = [],
|
|
146
|
+
evidence = new Map();
|
|
147
|
+
let sourceLiteralTruncated = false;
|
|
148
|
+
for (const [side, inventory] of [
|
|
149
|
+
["artifact", artifact],
|
|
150
|
+
["source", source],
|
|
151
|
+
]) {
|
|
152
|
+
let consumed = 0;
|
|
153
|
+
const entries = (inventory?.entries || [])
|
|
154
|
+
.filter((entry) => entry.type === "file")
|
|
155
|
+
.sort((a, b) =>
|
|
156
|
+
a.originalPath < b.originalPath
|
|
157
|
+
? -1
|
|
158
|
+
: a.originalPath > b.originalPath
|
|
159
|
+
? 1
|
|
160
|
+
: 0,
|
|
161
|
+
);
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const skip = (reason) =>
|
|
164
|
+
result.annotations.push({
|
|
165
|
+
type: "syntax-not-inspected",
|
|
166
|
+
side,
|
|
167
|
+
path: entry.path,
|
|
168
|
+
originalPath: entry.originalPath,
|
|
169
|
+
reason,
|
|
170
|
+
});
|
|
171
|
+
if (!/\.(?:mjs|cjs|js)$/.test(entry.path || entry.originalPath)) {
|
|
172
|
+
skip("unsupported-syntax");
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!Buffer.isBuffer(entry.content)) {
|
|
176
|
+
skip("not-retained");
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (entry.size > limits.fileBytes) {
|
|
180
|
+
skip("syntax-file-cap");
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (consumed + entry.size > limits.totalBytesPerSide) {
|
|
184
|
+
skip("syntax-total-cap");
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
consumed += entry.size;
|
|
188
|
+
let text, ast;
|
|
189
|
+
try {
|
|
190
|
+
text = new TextDecoder("utf-8", {
|
|
191
|
+
fatal: true,
|
|
192
|
+
ignoreBOM: true,
|
|
193
|
+
}).decode(entry.content);
|
|
194
|
+
const parseAs = (sourceType) => {
|
|
195
|
+
let tokens = 0;
|
|
196
|
+
return parse(text, {
|
|
197
|
+
ecmaVersion: 2025,
|
|
198
|
+
sourceType,
|
|
199
|
+
allowHashBang: true,
|
|
200
|
+
onToken() {
|
|
201
|
+
if (++tokens > limits.tokens) throw new Error("syntax-token-cap");
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
try {
|
|
206
|
+
ast = parseAs("module");
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
209
|
+
ast = parseAs("commonjs");
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
skip(
|
|
213
|
+
error.message === "syntax-token-cap" ? error.message : "parse-failed",
|
|
214
|
+
);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const coverage =
|
|
218
|
+
side === "artifact" ? result.coverage : result.sourceCoverage;
|
|
219
|
+
coverage.files++;
|
|
220
|
+
coverage.bytes += entry.size;
|
|
221
|
+
const positions = offsets(text);
|
|
222
|
+
for (const node of nodes(ast)) {
|
|
223
|
+
if (side === "source") {
|
|
224
|
+
if (
|
|
225
|
+
node.type === "Literal" &&
|
|
226
|
+
typeof node.value === "string" &&
|
|
227
|
+
!literals.has(node.value)
|
|
228
|
+
) {
|
|
229
|
+
if (literals.size < limits.sourceLiterals)
|
|
230
|
+
literals.set(
|
|
231
|
+
node.value,
|
|
232
|
+
evidenceFor(side, inventory, entry, node, positions),
|
|
233
|
+
);
|
|
234
|
+
else sourceLiteralTruncated = true;
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const kind = capability(node);
|
|
239
|
+
if (!kind) continue;
|
|
240
|
+
if (candidates.length >= limits.cues) {
|
|
241
|
+
skip("syntax-cue-cap");
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
candidates.push({
|
|
245
|
+
kind,
|
|
246
|
+
evidence: evidenceFor(side, inventory, entry, node, positions),
|
|
247
|
+
literal:
|
|
248
|
+
node.arguments?.[0]?.type === "Literal" &&
|
|
249
|
+
typeof node.arguments[0].value === "string"
|
|
250
|
+
? node.arguments[0].value
|
|
251
|
+
: null,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const stem = (value) => value.replace(/\.[cm]?[jt]sx?$/, "");
|
|
257
|
+
const unknownSource = new Set(
|
|
258
|
+
result.annotations
|
|
259
|
+
.filter(
|
|
260
|
+
(item) =>
|
|
261
|
+
item.type === "syntax-not-inspected" && item.side === "source",
|
|
262
|
+
)
|
|
263
|
+
.map((item) => stem(item.path || item.originalPath)),
|
|
264
|
+
);
|
|
265
|
+
for (const candidate of candidates) {
|
|
266
|
+
const ids = [candidate.evidence.id];
|
|
267
|
+
evidence.set(candidate.evidence.id, candidate.evidence);
|
|
268
|
+
const match = candidate.literal !== null && literals.get(candidate.literal);
|
|
269
|
+
if (match) {
|
|
270
|
+
evidence.set(match.id, match);
|
|
271
|
+
ids.push(match.id);
|
|
272
|
+
}
|
|
273
|
+
result.cues.push({
|
|
274
|
+
ruleId: `syntax-${candidate.kind}`,
|
|
275
|
+
priority: "review",
|
|
276
|
+
summary:
|
|
277
|
+
"Syntactic capability reference; execution and binding are not proven.",
|
|
278
|
+
literalStatus:
|
|
279
|
+
candidate.literal === null
|
|
280
|
+
? "no-static-literal"
|
|
281
|
+
: match
|
|
282
|
+
? "found-in-inspected-source"
|
|
283
|
+
: source
|
|
284
|
+
? result.sourceCoverage.files === 0 ||
|
|
285
|
+
sourceLiteralTruncated ||
|
|
286
|
+
unknownSource.has(
|
|
287
|
+
stem(
|
|
288
|
+
candidate.evidence.path || candidate.evidence.originalPath,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
? "not-assessed"
|
|
292
|
+
: "not-found-in-inspected-source"
|
|
293
|
+
: "source-unavailable",
|
|
294
|
+
evidenceIds: ids,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
result.evidence = [...evidence.values()];
|
|
298
|
+
result.annotations.push({
|
|
299
|
+
type: "syntax-scope",
|
|
300
|
+
parser: "acorn@8.19.0",
|
|
301
|
+
sourceCoverage: result.sourceCoverage,
|
|
302
|
+
sourceLiteralCount: literals.size,
|
|
303
|
+
sourceLiteralTruncated,
|
|
304
|
+
limitation:
|
|
305
|
+
"Literal matches are not semantic equivalence; missing literals refer only to inspected JavaScript. TypeScript, JSX, source maps, computed aliases and dynamic targets are not resolved.",
|
|
306
|
+
});
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function addSyntaxInspection(report, inspection) {
|
|
311
|
+
const result = structuredClone(report);
|
|
312
|
+
const existing = new Set(result.evidence.map((item) => item.id));
|
|
313
|
+
for (const item of inspection.evidence)
|
|
314
|
+
if (!existing.has(item.id)) {
|
|
315
|
+
result.evidence.push(item);
|
|
316
|
+
existing.add(item.id);
|
|
317
|
+
}
|
|
318
|
+
const byFile = new Map(result.files.map((file) => [file.originalPath, file]));
|
|
319
|
+
for (const item of inspection.evidence) {
|
|
320
|
+
const file = item.side === "artifact" && byFile.get(item.originalPath);
|
|
321
|
+
if (file && !file.evidenceIds.includes(item.id))
|
|
322
|
+
file.evidenceIds.push(item.id);
|
|
323
|
+
}
|
|
324
|
+
const evidenceById = new Map(
|
|
325
|
+
inspection.evidence.map((item) => [item.id, item]),
|
|
326
|
+
);
|
|
327
|
+
for (const cue of inspection.cues) {
|
|
328
|
+
const item = evidenceById.get(cue.evidenceIds[0]);
|
|
329
|
+
const file = byFile.get(item.originalPath);
|
|
330
|
+
const unchanged = file?.state === "byte-identical";
|
|
331
|
+
if (unchanged)
|
|
332
|
+
result.annotations.push({ type: "unchanged-capability", ...cue });
|
|
333
|
+
else result.cues.push(cue);
|
|
334
|
+
}
|
|
335
|
+
result.annotations.push(...inspection.annotations);
|
|
336
|
+
result.coverage.syntaxInspected = inspection.coverage;
|
|
337
|
+
result.inputs.limits = { ...result.inputs.limits, syntax: inspection.limits };
|
|
338
|
+
result.inputs.syntaxParser = "acorn@8.19.0";
|
|
339
|
+
return validateReport(result);
|
|
340
|
+
}
|