@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,255 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const css = `
|
|
4
|
+
:root{color-scheme:light;font-family:"Avenir Next","Segoe UI",sans-serif;color:#13294b;background:#edf4fa;font-size:16px;line-height:1.55}
|
|
5
|
+
*{box-sizing:border-box}body{margin:0}a{color:#156b85}a:focus-visible,input:focus-visible,summary:focus-visible{outline:3px solid #a65a12;outline-offset:3px}
|
|
6
|
+
header{padding:24px 5vw;background:#13294b;color:white;display:flex;justify-content:space-between;gap:24px;align-items:baseline}header strong{font-size:24px;letter-spacing:-1px}header p{margin:0;font-size:14px}
|
|
7
|
+
main{max-width:1440px;margin:0 auto;padding:42px 5vw;display:grid;grid-template-columns:240px minmax(0,1fr);gap:40px}aside{font-size:14px}aside dl{margin:20px 0}dt{font-weight:600;margin-top:16px}dd{margin:4px 0;overflow-wrap:anywhere}h1{font-size:clamp(24px,3vw,38px);line-height:1.15;font-weight:600;letter-spacing:-1px;margin:0 0 18px}h2{font-size:21px;margin:0 0 16px}h3{font-size:16px;margin:0 0 8px}p{max-width:75ch}
|
|
8
|
+
section{margin-bottom:30px}.status{display:inline-block;padding:4px 12px;background:#dceaf3;color:#13294b;border-radius:5px}.status.attention{background:#fff0da;color:#854300}.summary{background:white;padding:24px;border-radius:12px}.coverage{display:flex;gap:24px;flex-wrap:wrap;margin-top:20px}.coverage div{min-width:120px}.coverage strong{display:block;font-size:25px;line-height:1.2}.coverage span{font-size:13px}.note{font-size:14px;color:#405876}.finding{padding:12px 16px;border-left:4px solid #a65a12;background:#fff9ee;margin:8px 0}.finding code{overflow-wrap:anywhere}
|
|
9
|
+
label{font-size:14px;font-weight:600}input{display:block;margin:6px 0 18px;padding:11px 13px;width:100%;border:1px solid #9cb5ca;border-radius:6px;font:inherit;background:white;color:inherit}.files{background:white;border-radius:12px;overflow:hidden}.file{padding:14px 18px;border-bottom:1px solid #dce6ed}.file:last-child{border-bottom:0}.file[hidden]{display:none}.file summary{cursor:pointer;display:flex;gap:12px;align-items:baseline;flex-wrap:wrap}.path{flex:1;min-width:150px;font-weight:600;overflow-wrap:anywhere}.state{font-size:12px;padding:3px 7px;border-radius:4px;background:#e9e4f1;color:#55427a}.bytes{font-size:13px}.file dl{font-size:13px;display:grid;grid-template-columns:110px minmax(0,1fr);gap:6px 12px}.file dt,.file dd{margin:0}.file dd{overflow-wrap:anywhere}.evidence{padding:18px;background:white;border-radius:8px;margin:12px 0}.evidence h3{overflow-wrap:anywhere}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:13px/1.6 Menlo,Consolas,monospace;background:#eef3f8;padding:14px;border-radius:5px;max-height:500px;overflow:auto}code{font-family:Menlo,Consolas,monospace;font-size:0.9em}.model{background:#e9e4f1;padding:22px;border-radius:12px}.model p{margin-bottom:0}footer{padding:0 5vw 28px;text-align:center;font-size:13px;color:#405876}.empty{padding:18px;background:white;border-radius:8px}
|
|
10
|
+
.skip{position:absolute;left:16px;top:-100px;background:white;padding:10px}.skip:focus{top:10px}.evidence summary{cursor:pointer;font-weight:600;overflow-wrap:anywhere}.evidence:target{outline:3px solid #a65a12;outline-offset:3px}.evidence dl{font-size:13px}.citations{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:8px 20px;padding-left:20px;font-size:13px}.citations li{overflow-wrap:anywhere}.cue{background:#e8f2f6;border-left:4px solid #156b85;padding:16px 18px;margin:10px 0}.cue p{margin:8px 0}.scope{background:white;padding:18px;border-radius:8px}.inventory-row{padding:12px 0;border-bottom:1px solid #dce6ed;overflow-wrap:anywhere}.display-limit{padding:14px;border-left:4px solid #695290;background:#eee9f4;color:#493967}.file summary::after{content:"Open file details";font-size:12px;color:#405876}.file[open] summary::after{content:"Close file details"}
|
|
11
|
+
@media(max-width:800px){main{grid-template-columns:1fr;gap:22px;padding-top:26px}aside{border-bottom:1px solid #9cb5ca;padding-bottom:20px}aside dl{display:grid;grid-template-columns:100px minmax(0,1fr);gap:7px 12px}aside dt,aside dd{margin:0}header{align-items:flex-start;flex-direction:column;gap:4px}.file dl{grid-template-columns:1fr}.coverage{gap:16px}}
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const script = `
|
|
15
|
+
const input=document.getElementById("file-filter");
|
|
16
|
+
const rows=[...document.querySelectorAll(".file")];
|
|
17
|
+
document.addEventListener("click",event=>{
|
|
18
|
+
const link=event.target.closest("a[data-evidence]");
|
|
19
|
+
if(!link)return;
|
|
20
|
+
const target=document.getElementById(link.getAttribute("href").slice(1));
|
|
21
|
+
if(target?.tagName==="DETAILS")target.open=true;
|
|
22
|
+
});
|
|
23
|
+
input.addEventListener("input",()=>{
|
|
24
|
+
const query=input.value.toLocaleLowerCase();
|
|
25
|
+
let count=0;
|
|
26
|
+
for(const row of rows){row.hidden=!row.textContent.toLocaleLowerCase().includes(query);if(!row.hidden)count++;}
|
|
27
|
+
document.getElementById("visible-count").textContent=count+" files shown";
|
|
28
|
+
});
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
function escape(value) {
|
|
32
|
+
return String(value ?? "").replace(
|
|
33
|
+
/[&<>"']/g,
|
|
34
|
+
(char) =>
|
|
35
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
36
|
+
char
|
|
37
|
+
],
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bytes(value) {
|
|
42
|
+
return `${Number.isSafeInteger(value) && value >= 0 ? value.toLocaleString("en-US") : "unknown"} bytes`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assetHash(value) {
|
|
46
|
+
return createHash("sha256").update(value).digest("base64");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MiB = 1024 * 1024;
|
|
50
|
+
const maxFieldChars = 4096;
|
|
51
|
+
|
|
52
|
+
function text(value, limit = maxFieldChars) {
|
|
53
|
+
const string = String(value ?? "");
|
|
54
|
+
return (
|
|
55
|
+
escape(string.slice(0, limit)) +
|
|
56
|
+
(string.length > limit ? "… [text truncated; see full JSON report]" : "")
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function range(value) {
|
|
61
|
+
return Array.isArray(value) &&
|
|
62
|
+
value.length === 2 &&
|
|
63
|
+
value.every((number) => Number.isSafeInteger(number) && number >= 0)
|
|
64
|
+
? `[${value[0]}, ${value[1]})`
|
|
65
|
+
: "Unavailable";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function collection(items, render, maxBytes, maxItems = 20000) {
|
|
69
|
+
const chunks = [];
|
|
70
|
+
let used = 0;
|
|
71
|
+
for (let i = 0; i < Math.min(items.length, maxItems); i++) {
|
|
72
|
+
const html = render(items[i], i);
|
|
73
|
+
const size = Buffer.byteLength(html);
|
|
74
|
+
if (used + size > maxBytes) break;
|
|
75
|
+
used += size;
|
|
76
|
+
chunks.push(html);
|
|
77
|
+
}
|
|
78
|
+
const shown = chunks.length;
|
|
79
|
+
const body = chunks.join("");
|
|
80
|
+
const notice =
|
|
81
|
+
shown < items.length
|
|
82
|
+
? `<p class="display-limit">HTML display limit: ${shown} of ${items.length} entries shown in this section. Remaining entries are preserved in the full JSON report.</p>`
|
|
83
|
+
: "";
|
|
84
|
+
return { shown, body, notice, html: body + notice };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function excerptView(evidence) {
|
|
88
|
+
if (typeof evidence.excerpt !== "string")
|
|
89
|
+
return '<p class="note">Text excerpt unavailable. Content may be unretained or not valid UTF-8; inspect the original digest-bound input.</p>';
|
|
90
|
+
let shown = "",
|
|
91
|
+
size = 0;
|
|
92
|
+
for (const character of evidence.excerpt) {
|
|
93
|
+
const length = Buffer.byteLength(character);
|
|
94
|
+
if (size + length > 512) break;
|
|
95
|
+
shown += character;
|
|
96
|
+
size += length;
|
|
97
|
+
}
|
|
98
|
+
const start = evidence.excerptRange?.[0];
|
|
99
|
+
const shownRange =
|
|
100
|
+
Number.isSafeInteger(start) && start >= 0 ? [start, start + size] : null;
|
|
101
|
+
const partial =
|
|
102
|
+
shown.length < evidence.excerpt.length ||
|
|
103
|
+
(shownRange && shownRange[1] < evidence.range?.[1]);
|
|
104
|
+
return `<p class="note">Shown excerpt byte range ${range(shownRange)} (${size} bytes).${partial ? " Remaining evidence bytes are not displayed; inspect the original digest-bound input." : ""}</p><pre>${escape(shown)}</pre>`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function evidenceView(entry) {
|
|
108
|
+
const evidence = entry.value;
|
|
109
|
+
return `<details class="evidence" id="${entry.target}"><summary>${text(evidence.side === "artifact" ? "Artifact" : evidence.side === "source" ? "Source" : evidence.side)}: ${text(evidence.path ?? evidence.originalPath)}</summary><p class="note">Evidence ${text(evidence.id)}. Evidence byte range ${range(evidence.range)}</p>${excerptView(evidence)}<dl><dt>Original path</dt><dd>${text(evidence.originalPath)}</dd><dt>Input blob SHA-256</dt><dd><code>${text(evidence.blobDigest || "Unavailable")}</code></dd><dt>File SHA-256</dt><dd><code>${text(evidence.fileSha256 || "Unavailable")}</code></dd></dl></details>`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function citations(ids, index) {
|
|
113
|
+
if (!Array.isArray(ids) || ids.length === 0)
|
|
114
|
+
return '<p class="note">No bound citations recorded.</p>';
|
|
115
|
+
const references = [...new Set(ids.slice(0, 8))]
|
|
116
|
+
.map((id) => {
|
|
117
|
+
const entry = index.get(id);
|
|
118
|
+
if (!entry?.shown)
|
|
119
|
+
return `<li>Evidence ${text(id)} is not shown here; see the full JSON report.</li>`;
|
|
120
|
+
const evidence = entry.value;
|
|
121
|
+
const side =
|
|
122
|
+
evidence.side === "artifact"
|
|
123
|
+
? "Artifact"
|
|
124
|
+
: evidence.side === "source"
|
|
125
|
+
? "Source"
|
|
126
|
+
: "Evidence";
|
|
127
|
+
return `<li><a data-evidence href="#${entry.target}">${side}: ${text(evidence.path ?? evidence.originalPath)}; bytes ${range(evidence.range)}</a> <code>${text(id)}</code></li>`;
|
|
128
|
+
})
|
|
129
|
+
.join("");
|
|
130
|
+
return `<ul class="citations">${references}</ul>${ids.length > 8 ? '<p class="display-limit">HTML display limit: first eight citation references shown. See the full JSON report for all references.</p>' : ""}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function cueView(cue, index) {
|
|
134
|
+
const labels = {
|
|
135
|
+
"found-in-inspected-source": "Literal found in inspected source",
|
|
136
|
+
"not-found-in-inspected-source": "Literal not found in inspected source",
|
|
137
|
+
"source-unavailable": "Source unavailable for this observation",
|
|
138
|
+
"no-static-literal": "No static literal was available",
|
|
139
|
+
};
|
|
140
|
+
return `<article class="cue"><h3><code>${text(cue.ruleId)}</code></h3><p>${text(cue.summary || cue.message, 1024)}</p>${cue.literalStatus !== undefined ? `<p class="note">${text(labels[cue.literalStatus] || "Source literal status unavailable")} <code>${text(cue.literalStatus)}</code></p>` : ""}${citations(cue.evidenceIds, index)}</article>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function annotationView(item, index) {
|
|
144
|
+
const kind = item.type || item.code || item.ruleId || "Inspection annotation";
|
|
145
|
+
const path = item.path ?? item.originalPath;
|
|
146
|
+
const description =
|
|
147
|
+
item.summary ||
|
|
148
|
+
item.message ||
|
|
149
|
+
item.limitation ||
|
|
150
|
+
item.reason ||
|
|
151
|
+
item.scope;
|
|
152
|
+
const scope =
|
|
153
|
+
kind === "syntax-scope"
|
|
154
|
+
? `<dl><dt>Parser</dt><dd>${text(item.parser || "Unavailable")}</dd><dt>Source syntax inspected</dt><dd>${bytes(item.sourceCoverage?.bytes)} across ${text(item.sourceCoverage?.files ?? "unknown")} files.</dd><dt>Source literals observed</dt><dd>${text(item.sourceLiteralCount ?? "unknown")}</dd></dl>${item.sourceLiteralTruncated ? '<p class="note">Source literal inventory was truncated; missing literals refer only to the inspected subset.</p>' : ""}`
|
|
155
|
+
: "";
|
|
156
|
+
return `<article class="inventory-row"><h3><code>${text(kind)}</code></h3>${path ? `<p>${item.side ? `${text(item.side)}: ` : ""}${text(path)}</p>` : ""}<p>${text(description)}</p>${item.reason && (item.summary || item.message || item.limitation) ? `<p class="note">${text(item.reason)}</p>` : ""}${scope}${Array.isArray(item.evidenceIds) && item.evidenceIds.length ? citations(item.evidenceIds, index) : ""}</article>`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function renderReport(report) {
|
|
160
|
+
const claim = report.inputs?.claim || report.manifests?.artifact?.value || {};
|
|
161
|
+
const identity = claim.name
|
|
162
|
+
? `${String(claim.name).slice(0, maxFieldChars)}${claim.version ? `@${String(claim.version).slice(0, 300)}` : ""}`
|
|
163
|
+
: "local artifact";
|
|
164
|
+
const code = report.policy?.exitCode;
|
|
165
|
+
const status =
|
|
166
|
+
code === 2
|
|
167
|
+
? "Incomplete analysis"
|
|
168
|
+
: code === 3
|
|
169
|
+
? "Review required"
|
|
170
|
+
: "Completed report";
|
|
171
|
+
const inventory = report.coverage?.inventory || {};
|
|
172
|
+
const findings = report.policy?.findings || [];
|
|
173
|
+
const files = report.files || [];
|
|
174
|
+
const evidence = report.evidence || [];
|
|
175
|
+
const index = new Map();
|
|
176
|
+
const indexed = [];
|
|
177
|
+
for (let i = 0; i < Math.min(evidence.length, 50000); i++) {
|
|
178
|
+
const item = evidence[i];
|
|
179
|
+
if (index.has(item.id)) continue;
|
|
180
|
+
const entry = { value: item, target: `evidence-${i}`, shown: false };
|
|
181
|
+
index.set(item.id, entry);
|
|
182
|
+
indexed.push(entry);
|
|
183
|
+
}
|
|
184
|
+
const catalog = collection(indexed, evidenceView, 6 * MiB, 50000);
|
|
185
|
+
for (let i = 0; i < catalog.shown; i++) indexed[i].shown = true;
|
|
186
|
+
if (evidence.length > 50000)
|
|
187
|
+
catalog.html +=
|
|
188
|
+
'<p class="display-limit">HTML display limit: at most 50,000 evidence records are indexed. See the full JSON report.</p>';
|
|
189
|
+
const fileRows = collection(
|
|
190
|
+
files,
|
|
191
|
+
(file) =>
|
|
192
|
+
`<details class="file"><summary><span class="path">${text(file.path ?? file.originalPath)}</span><span class="state">${text(file.state)}</span><span class="bytes">${bytes(file.size)}</span></summary><dl><dt>Original path</dt><dd>${text(file.originalPath)}</dd><dt>Reason</dt><dd>${text(file.reason || "Observed byte correspondence only")}</dd><dt>Artifact SHA-256</dt><dd><code>${text(file.artifactSha256)}</code></dd><dt>Source SHA-256</dt><dd><code>${text(file.sourceSha256 || "Unavailable")}</code></dd><dt>Localization</dt><dd>${text(typeof file.localization === "string" ? file.localization : "See bound evidence ranges")}</dd></dl>${citations(file.evidenceIds, index)}</details>`,
|
|
193
|
+
6 * MiB,
|
|
194
|
+
);
|
|
195
|
+
const findingRows = collection(
|
|
196
|
+
findings,
|
|
197
|
+
(finding) =>
|
|
198
|
+
`<div class="finding"><code>${text(finding.ruleId || finding.id)}</code><p class="note">${text(finding.message || "Review the bound artifact and source evidence.", 1024)}</p>${citations(finding.evidenceIds, index)}</div>`,
|
|
199
|
+
MiB / 2,
|
|
200
|
+
);
|
|
201
|
+
const failures = collection(
|
|
202
|
+
report.failures || [],
|
|
203
|
+
(failure) =>
|
|
204
|
+
`<div class="finding"><code>${text(failure.code)}</code><p>${text(failure.message, 1024)}</p></div>`,
|
|
205
|
+
MiB / 4,
|
|
206
|
+
);
|
|
207
|
+
const cues = collection(
|
|
208
|
+
report.cues || [],
|
|
209
|
+
(cue) => cueView(cue, index),
|
|
210
|
+
MiB,
|
|
211
|
+
10000,
|
|
212
|
+
);
|
|
213
|
+
const annotations = collection(
|
|
214
|
+
report.annotations || [],
|
|
215
|
+
(item) => annotationView(item, index),
|
|
216
|
+
MiB / 2,
|
|
217
|
+
);
|
|
218
|
+
const links = collection(
|
|
219
|
+
report.coverage?.linkEntries || [],
|
|
220
|
+
(link) =>
|
|
221
|
+
`<article class="inventory-row"><h3>${text(link.side)}: ${text(link.path ?? link.originalPath)}</h3><p>${text(link.type)}. Inert target: <code>${text(link.linkTarget)}</code></p><p class="note">Original path: ${text(link.originalPath)}. Link content is not followed or compared.</p></article>`,
|
|
222
|
+
MiB / 2,
|
|
223
|
+
);
|
|
224
|
+
const sourceOnly = collection(
|
|
225
|
+
report.coverage?.sourceOnly || [],
|
|
226
|
+
(path) => `<li>${text(path)}</li>`,
|
|
227
|
+
MiB / 2,
|
|
228
|
+
);
|
|
229
|
+
const model = report.model || { status: "disabled" };
|
|
230
|
+
const hypotheses = collection(
|
|
231
|
+
model.hypotheses || [],
|
|
232
|
+
(hypothesis) =>
|
|
233
|
+
`<article><p>${text(hypothesis.explanation, 4096)}</p>${citations(hypothesis.evidenceIds, index)}</article>`,
|
|
234
|
+
MiB / 4,
|
|
235
|
+
1000,
|
|
236
|
+
);
|
|
237
|
+
const modelText =
|
|
238
|
+
model.status === "disabled"
|
|
239
|
+
? "Model is disabled"
|
|
240
|
+
: `Model status: ${model.status}`;
|
|
241
|
+
const csp = `default-src 'none'; script-src 'sha256-${assetHash(script)}'; style-src 'sha256-${assetHash(css)}'; connect-src 'none'; base-uri 'none'; form-action 'none'`;
|
|
242
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="${escape(csp)}"><meta name="referrer" content="no-referrer"><meta name="robots" content="noindex"><title>UNMASK · ${text(identity)}</title><style>${css}</style></head><body>
|
|
243
|
+
<a class="skip" href="#content">Skip to evidence</a><header><strong>UNMASK</strong><p>Published bytes. Source claims. Evidence to review.</p></header>
|
|
244
|
+
<main id="content"><aside><h2>Release identity</h2><dl><dt>Artifact SHA-256</dt><dd><code>${text(report.inputs?.artifact?.sha256)}</code></dd><dt>Source selection</dt><dd>${text(report.source?.status || "unresolved")}</dd><dt>Source method</dt><dd>${text(report.source?.method || "unresolved")}</dd><dt>Commit claim</dt><dd><code>${text(report.source?.commit || "Unavailable")}</code></dd></dl><p>Matching bytes establish correspondence to a selected source. They do not establish benign behavior.</p></aside>
|
|
245
|
+
<div><section class="summary"><h1>Review evidence for ${text(identity)}</h1><span class="status ${code === 2 || code === 3 ? "attention" : ""}">${status}</span><p>A completed report is not a safety verdict. Unknown and uninspected content stays visible.</p><div class="coverage"><div><strong>${bytes(inventory.bytes)}</strong><span>${inventory.complete === false || inventory.lowerBound ? "known lower bound" : "artifact inventory"}</span></div><div><strong>${bytes(report.coverage?.compared?.bytes)}</strong><span>compared with selected source</span></div><div><strong>${text(inventory.files ?? 0)}</strong><span>regular files inventoried</span></div></div></section>
|
|
246
|
+
<section><h2>${findings.length ? "Review required" : "Policy findings"}</h2>${findings.length ? findingRows.html : '<p class="empty">No default policy findings. This does not mean no review is needed.</p>'}${failures.html}</section>
|
|
247
|
+
<section><h2>${(report.cues || []).length ? "Review suggested" : "Review observations"}</h2><p class="note">Observations support review. They do not establish execution or maliciousness. Syntax literal status applies only to inspected JavaScript.</p>${(report.cues || []).length ? cues.html : '<p class="empty">No review suggestions recorded. Uninspected content remains unknown.</p>'}</section>
|
|
248
|
+
<section><h2>File correspondence</h2><label for="file-filter">Find a path, state or digest</label><input id="file-filter" type="search" autocomplete="off" placeholder="Filter files"><p id="visible-count" class="note" aria-live="polite">${fileRows.shown} files shown</p><div class="files">${files.length ? fileRows.html : '<p class="empty">No regular files were accepted. Check the analysis limitations above.</p>'}</div></section>
|
|
249
|
+
<section><h2>Inspection scope and limits</h2><div class="scope"><p>Syntax inspected: ${bytes(report.coverage?.syntaxInspected?.bytes)} across ${text(report.coverage?.syntaxInspected?.files ?? "unknown")} files.</p><p>Model reviewed: ${bytes(report.coverage?.modelReviewed?.bytes)} across ${text(report.coverage?.modelReviewed?.files ?? "unknown")} files.</p><p>Inspection coverage is separate from byte correspondence. Unsupported syntax, missing content and skipped maps remain uninspected.</p>${annotations.html || '<p class="note">No inspection annotations recorded.</p>'}</div></section>
|
|
250
|
+
<section><h2>Inert links</h2><p class="note">Artifact links: ${text(report.coverage?.links?.artifact ?? "unknown")}. Source links: ${text(report.coverage?.links?.source ?? "unknown")}.</p>${links.html || '<p class="empty">No inert link entries recorded.</p>'}</section>
|
|
251
|
+
<section><h2>Source-only paths</h2>${sourceOnly.html ? `<ul>${sourceOnly.body}</ul>${sourceOnly.notice}` : '<p class="empty">No source-only paths recorded. Source availability and completeness still apply.</p>'}</section>
|
|
252
|
+
<section><h2>Bound evidence</h2><p class="note">Excerpts show original text data. Excerpt display limit: 512 bytes per evidence item. Byte ranges are half-open: start included, end excluded. Open a citation to inspect its digest-bound evidence.</p>${catalog.html || '<p class="empty">No bound evidence recorded.</p>'}</section>
|
|
253
|
+
<section class="model"><h2>Model explanations</h2><p>${text(modelText)}</p><p class="note">Hypotheses are separate from deterministic evidence. A valid citation proves its location, not the reasoning.</p>${hypotheses.html}</section>
|
|
254
|
+
</div></main><footer>UNMASK presents evidence for review. It never declares a package safe. HTML display limits do not change the full JSON report.</footer><script>${script}</script></body></html>`;
|
|
255
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const css = `
|
|
4
|
+
:root{font-family:"Avenir Next","Segoe UI",sans-serif;color:#13294b;background:#edf4fa;font-size:16px;line-height:1.6;color-scheme:light}*{box-sizing:border-box}body{margin:0}a{color:#156b85;text-underline-offset:3px}a:focus-visible,input:focus-visible,button:focus-visible{outline:3px solid #a65a12;outline-offset:4px}.skip{position:absolute;left:16px;top:-100px;background:white;padding:10px}.skip:focus{top:10px}header{background:#13294b;color:white;padding:24px max(5vw,24px);display:flex;align-items:baseline;justify-content:space-between;gap:24px}header a{color:inherit;text-decoration:none;font-size:24px;font-weight:750;letter-spacing:-1px}header p{margin:0;font-size:14px}main{max-width:1220px;margin:auto;padding:64px max(5vw,24px);display:grid;grid-template-columns:220px minmax(0,1fr);gap:48px}aside{border-top:3px solid #156b85;padding-top:20px;font-size:14px}aside p{margin:0 0 18px}aside strong{font-weight:650}h1{font-size:clamp(32px,4vw,52px);font-weight:600;line-height:1.13;letter-spacing:-1.5px;margin:0 0 24px;max-width:18ch}h2{font-size:19px;font-weight:650;line-height:1.3;margin:0 0 12px}p{max-width:64ch}form{background:#fff;padding:28px;margin:32px 0 26px;border-radius:12px;border:1px solid #cfdfeb}label{display:block;font-weight:650;margin-bottom:10px}.entry{display:flex;gap:12px;align-items:stretch}input{font:inherit;min-width:0;flex:1;border:1px solid #7895ad;background:white;padding:13px 14px;color:inherit;border-radius:6px}button{font:inherit;font-weight:650;cursor:pointer;color:white;background:#156b85;border:1px solid #156b85;border-radius:6px;padding:13px 22px}button:hover{background:#10576d}.hint{font-size:14px;color:#405876;margin:10px 0 0}.error{color:#854300;background:#fff0da;border-left:4px solid #a65a12;padding:14px 18px;overflow-wrap:anywhere}.ledger{background:white;border-top:3px solid #156b85;border-radius:0 0 8px 8px;margin-top:32px;padding:0 24px}.row{display:grid;grid-template-columns:160px minmax(0,1fr);gap:24px;border-bottom:1px solid #dce6ed;padding:18px 0}.row:last-child{border-bottom:0}.row strong{font-weight:650}.row span{font-size:14px;color:#405876}.status{display:inline-block;padding:5px 14px;background:#dceaf3;color:#13294b;border-radius:5px;font-weight:650}.status.incomplete,.status.failed{background:#fff0da;color:#854300}.identity{font-size:21px;overflow-wrap:anywhere}.meta{font-size:14px;color:#405876}time,code{overflow-wrap:anywhere}code{font-family:Menlo,monospace;font-size:.9em}footer{max-width:1220px;margin:auto;padding:0 max(5vw,24px) 32px;font-size:13px;color:#405876}footer p{max-width:95ch}.links{display:flex;flex-wrap:wrap;gap:24px;margin-top:24px}@media(max-width:760px){header{align-items:flex-start;flex-direction:column;gap:4px}main{grid-template-columns:1fr;gap:24px;padding-top:32px}aside{display:flex;gap:24px}aside p{flex:1}.entry{flex-direction:column}form{padding:20px}h1{max-width:22ch}.row{grid-template-columns:1fr;gap:4px}.ledger{padding:0 18px}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto}}
|
|
5
|
+
`;
|
|
6
|
+
const escape = (value) =>
|
|
7
|
+
String(value ?? "").replace(
|
|
8
|
+
/[&<>"']/g,
|
|
9
|
+
(ch) =>
|
|
10
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
11
|
+
ch
|
|
12
|
+
],
|
|
13
|
+
);
|
|
14
|
+
function layout(title, body, { refresh = false } = {}) {
|
|
15
|
+
const hash = createHash("sha256").update(css).digest("base64");
|
|
16
|
+
const csp = `default-src 'none'; style-src 'sha256-${hash}'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'`;
|
|
17
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><meta name="robots" content="noindex"><meta http-equiv="Content-Security-Policy" content="${escape(csp)}">${refresh ? '<meta http-equiv="refresh" content="3">' : ""}<title>UNMASK · ${escape(title)}</title><style>${css}</style></head><body><a class="skip" href="#content">Skip to content</a><header><a href="/" aria-label="UNMASK home">UNMASK</a><p>Published bytes. Source claims. Evidence to review.</p></header><main id="content"><aside><p><strong>Release evidence</strong><br>Inspect a published package before accepting an update.</p><p>Matching source bytes do not establish benign behavior.</p></aside><div>${body}</div></main><footer><p>UNMASK shows evidence for review and never declares a package safe.</p><p>Completed results are kept for up to 30 minutes after completion, subject to capacity eviction or restart. Viewing a result does not extend its lifetime. Save the JSON report to keep the evidence.</p><p><a href="https://github.com/Vaiditya2207/unmask" rel="noreferrer">Open source on GitHub</a> (MIT)</p></footer></body></html>`;
|
|
18
|
+
}
|
|
19
|
+
export function renderHome({ value = "", error = "", retryAfter } = {}) {
|
|
20
|
+
return layout(
|
|
21
|
+
"Check a release",
|
|
22
|
+
`<h1>What shipped beyond the source?</h1><p>Check an exact npm release. Review published files, source correspondence and the evidence behind each finding.</p>${error ? `<p class="error" role="alert" id="input-error">${escape(error)}${retryAfter ? ` Try again in ${escape(retryAfter)} seconds.` : ""}</p>` : ""}<form action="/check" method="post"><label for="package">Package name and exact version</label><div class="entry"><input id="package" name="package" value="${escape(value)}" placeholder="@scope/package@1.2.3" required maxlength="300" autocomplete="off" autocapitalize="none" spellcheck="false" aria-describedby="package-hint${error ? " input-error" : ""}"${error ? ' aria-invalid="true"' : ""}><button type="submit" aria-describedby="check-data-flow">Check release</button></div><p class="hint" id="package-hint">Use an exact version, such as chalk@6.0.1. Tags, version ranges, URLs and uploads are not accepted.</p><p class="hint" id="check-data-flow">A check sends the package name and version to our server. The server may send bounded public package and source excerpts to Google for advisory explanations. No API key is needed in your browser.</p></form><p class="hint">Checks share a global limit of six new analyses per ten minutes. Identical in-flight checks share work. Eligible results can be reused; unavailable model explanations and other retryable failures need a new attempt.</p><div class="ledger" aria-label="What the report contains"><div class="row"><strong>Release identity</strong><span>Original artifact digests and the source claim selected for comparison.</span></div><div class="row"><strong>File correspondence</strong><span>Changed, artifact-only, byte-identical and not-compared files, with byte counts and inspection limits.</span></div><div class="row"><strong>Review evidence</strong><span>Deterministic findings first. Model explanations, when available, remain separate hypotheses.</span></div></div>`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
export function renderStatus(job) {
|
|
26
|
+
const messages = {
|
|
27
|
+
queued:
|
|
28
|
+
"Your check is waiting for the active analysis to finish. This page refreshes automatically.",
|
|
29
|
+
running:
|
|
30
|
+
"The release is being inspected. This page refreshes automatically; reloading does not start another check.",
|
|
31
|
+
incomplete:
|
|
32
|
+
"The check could not finish within its limits. No automatic retry will run. Return to the form to request a new check.",
|
|
33
|
+
failed:
|
|
34
|
+
"The analysis could not produce a report. Return to the form to request a new check.",
|
|
35
|
+
};
|
|
36
|
+
const status = ["queued", "running", "incomplete", "failed"].includes(
|
|
37
|
+
job.status,
|
|
38
|
+
)
|
|
39
|
+
? job.status
|
|
40
|
+
: "failed";
|
|
41
|
+
const time = Number.isFinite(job.createdAt)
|
|
42
|
+
? new Date(job.createdAt).toISOString()
|
|
43
|
+
: "";
|
|
44
|
+
return layout(
|
|
45
|
+
`${status} check`,
|
|
46
|
+
`<h1>Release check</h1><p class="identity">${escape(job.nameAtExactVersion)}</p><p class="status ${status}" role="status">${status[0].toUpperCase() + status.slice(1)}</p><p>${messages[status]}</p>${job.reason ? `<p class="meta">Reason: <code>${escape(job.reason)}</code></p>` : ""}<p class="meta">Requested <time datetime="${time}">${time}</time></p><div class="links"><a href="/">Check another release</a><a href="/r/${escape(job.id)}.json">View job JSON</a></div>`,
|
|
47
|
+
{ refresh: ["queued", "running"].includes(status) },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
export function renderError({
|
|
51
|
+
title = "Request unavailable",
|
|
52
|
+
message = "Return to the form and try again.",
|
|
53
|
+
} = {}) {
|
|
54
|
+
return layout(
|
|
55
|
+
title,
|
|
56
|
+
`<h1>${escape(title)}</h1><p class="error" role="alert">${escape(message)}</p><a href="/">Return to the release form</a>`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
export function renderCompleted(reportHtml, job) {
|
|
60
|
+
const time = new Date(job.finishedAt).toISOString();
|
|
61
|
+
const navigation = `<nav class="summary" aria-label="Report actions"><a href="/">Check another release</a> | <a href="/r/${escape(job.id)}.json" download="unmask-report.json">Download report JSON</a><p>Cached analysis: <time datetime="${time}">${time}</time>. Eligible results may be reused for up to 30 minutes after completion. Capacity eviction or restart can remove them sooner. Viewing a result does not extend this period. Unavailable model explanations and other retryable failures need a new attempt.</p></nav>`;
|
|
62
|
+
if (/<\/header\s*>/i.test(reportHtml))
|
|
63
|
+
return reportHtml.replace(
|
|
64
|
+
/<\/header\s*>/i,
|
|
65
|
+
(closing) => closing + navigation,
|
|
66
|
+
);
|
|
67
|
+
if (/<\/body\s*>/i.test(reportHtml))
|
|
68
|
+
return reportHtml.replace(
|
|
69
|
+
/<\/body\s*>/i,
|
|
70
|
+
(closing) => navigation + closing,
|
|
71
|
+
);
|
|
72
|
+
return reportHtml + navigation;
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xagents_in/unmask",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Evidence-based comparison of npm packages and their source archives",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Vaiditya2207/unmask.git"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": "24.22.2"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "node scripts/build.mjs",
|
|
16
|
+
"check": "prettier --check .",
|
|
17
|
+
"format": "prettier --write .",
|
|
18
|
+
"test": "node --test tests/tooling/*.test.mjs tests/product/*.test.mjs",
|
|
19
|
+
"test:product": "node --test tests/product/*.test.mjs",
|
|
20
|
+
"check:package": "node scripts/check-package.mjs --build",
|
|
21
|
+
"test:release": "node scripts/smoke-candidate.mjs",
|
|
22
|
+
"release:readiness": "node scripts/release-readiness.mjs"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"prettier": "3.9.10"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"bin": {
|
|
31
|
+
"unmask": "dist/cli/main.mjs"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"acorn": "8.19.0",
|
|
35
|
+
"tar-stream": "3.2.2"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"registry": "https://registry.npmjs.org/",
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|