@xagents_in/unmask 0.1.2 → 0.1.4

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.
Files changed (56) hide show
  1. package/README.md +301 -85
  2. package/dist/cli/annotations.mjs +145 -0
  3. package/dist/cli/audit.mjs +223 -0
  4. package/dist/cli/batch.mjs +218 -0
  5. package/dist/cli/cache.mjs +488 -0
  6. package/dist/cli/compare.mjs +261 -0
  7. package/dist/cli/config.mjs +141 -0
  8. package/dist/cli/discovery.mjs +372 -0
  9. package/dist/cli/doctor.mjs +354 -0
  10. package/dist/cli/exceptions.mjs +170 -0
  11. package/dist/cli/file.mjs +271 -0
  12. package/dist/cli/files.mjs +141 -0
  13. package/dist/cli/history.mjs +232 -0
  14. package/dist/cli/hosted.mjs +251 -43
  15. package/dist/cli/info.mjs +125 -0
  16. package/dist/cli/init.mjs +57 -0
  17. package/dist/cli/install-guard.mjs +896 -0
  18. package/dist/cli/lockfile.mjs +330 -0
  19. package/dist/cli/main.mjs +101 -14
  20. package/dist/cli/preflight.mjs +388 -0
  21. package/dist/cli/sarif.mjs +100 -0
  22. package/dist/cli/scan.mjs +128 -0
  23. package/dist/cli/shell-integration.mjs +122 -0
  24. package/dist/cli/terminal.mjs +396 -0
  25. package/dist/cli/watch.mjs +233 -0
  26. package/dist/cli/why.mjs +256 -0
  27. package/dist/core/advisories.mjs +101 -24
  28. package/dist/core/advisory-cache.mjs +295 -0
  29. package/dist/core/behavior-signals.mjs +1088 -0
  30. package/dist/core/dependency-graph.mjs +332 -0
  31. package/dist/core/evidence-exports.mjs +469 -0
  32. package/dist/core/guard-config.mjs +463 -0
  33. package/dist/core/guard-policy.mjs +308 -0
  34. package/dist/core/package-details.mjs +42 -6
  35. package/dist/core/registry-facts.mjs +342 -0
  36. package/dist/core/report-exploration.mjs +328 -0
  37. package/dist/core/report.mjs +7 -0
  38. package/dist/core/sarif.mjs +556 -0
  39. package/dist/core/syntax.mjs +296 -15
  40. package/dist/model/client.mjs +204 -56
  41. package/dist/model/diff-evidence.mjs +177 -0
  42. package/dist/model/summary.mjs +183 -0
  43. package/dist/server/advisory-result.mjs +137 -0
  44. package/dist/server/app.mjs +66 -2
  45. package/dist/server/dependency-screening.mjs +386 -0
  46. package/dist/server/exports.mjs +63 -0
  47. package/dist/server/guard-decision.mjs +37 -0
  48. package/dist/server/main.mjs +25 -7
  49. package/dist/server/queue.mjs +95 -53
  50. package/dist/server/registry-facts.mjs +118 -0
  51. package/dist/server/report-exploration.mjs +87 -0
  52. package/dist/server/store.mjs +21 -1
  53. package/dist/version.json +1 -1
  54. package/dist/viewer/render.mjs +10 -2
  55. package/dist/viewer/shell.mjs +1 -1
  56. package/package.json +1 -1
package/README.md CHANGED
@@ -1,31 +1,220 @@
1
+ <div align="center">
2
+
1
3
  # UNMASK
2
4
 
3
- See what actually shipped.
5
+ ### See what actually shipped.
6
+
7
+ **The code you review on GitHub is not the code you install from npm.**<br>
8
+ UNMASK compares the two, byte for byte, and shows the evidence for every difference.
9
+
10
+ [![npm](https://img.shields.io/npm/v/@xagents_in/unmask?color=dafa9c&labelColor=0f0f0f&label=npm)](https://www.npmjs.com/package/@xagents_in/unmask)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-dafa9c?labelColor=0f0f0f)](LICENSE)
12
+ [![Node 24.22.2](https://img.shields.io/badge/node-24.22.2-dafa9c?labelColor=0f0f0f)](.nvmrc)
13
+ [![Hacktoberfest 2026](https://img.shields.io/badge/Hacktoberfest%202026-Problem%2004-dafa9c?labelColor=0f0f0f)](HACKATHON.md)
14
+
15
+ [**Try the website**](https://unmask-web-386156642190.asia-south1.run.app) ·
16
+ [Analysis API](https://unmask-386156642190.asia-south1.run.app/version) ·
17
+ [npm package](https://www.npmjs.com/package/@xagents_in/unmask) ·
18
+ [Engineering docs](docs/README.md)
19
+
20
+ </div>
21
+
22
+ ---
23
+
24
+ ## The problem in 30 seconds
25
+
26
+ Developers review source code. Machines install the **published tarball**. Nothing
27
+ forces the two to match: npm checks the tarball's hash, never its source.
28
+
29
+ Attackers know this. Some of the worst supply-chain attacks hid their payload
30
+ **only in the published package**:
31
+
32
+ | Year | Package | What shipped that was never committed |
33
+ | ---- | ----------------------- | ------------------------------------------ |
34
+ | 2018 | `flatmap-stream 0.1.1` | A payload only in the minified npm file |
35
+ | 2018 | `eslint-scope 3.7.2` | A `postinstall` that stole `.npmrc` tokens |
36
+ | 2024 | `xz-utils 5.6.0` | A backdoor only in the release tarballs |
37
+ | 2025 | `@ctrl/tinycolor 4.1.1` | A `bundle.js` added by the Shai-Hulud worm |
38
+
39
+ Existing tools answer other questions. Vulnerability databases ask _"is this
40
+ version known bad?"_. Behaviour scanners ask _"does it look malicious?"_. npm
41
+ provenance asks _"where was it built?"_. **UNMASK asks: "what shipped that is not in
42
+ the source, and where exactly?"**
43
+
44
+ ## Try it in one minute
45
+
46
+ **On the web:** open the [website](https://unmask-web-386156642190.asia-south1.run.app),
47
+ type an exact release such as `chalk@6.0.1`, and press **check it**.
48
+
49
+ **From a terminal:**
50
+
51
+ ```bash
52
+ npx @xagents_in/unmask check chalk@6.0.1
53
+ ```
54
+
55
+ The terminal shows the result, coverage, findings, known advisories, the AI
56
+ explanation status and next steps. Add `--out ./unmask-report` to also save
57
+ `report.json` and an escaped `report.html`; nothing is written without it. Only the
58
+ package name and version leave your machine; the hosted service does the rest.
59
+
60
+ ### What the terminal shows
61
+
62
+ The examples below are real CLI output from a local test server with synthetic
63
+ fixtures and recorded OSV advisory data. They are not live-service results.
64
+
65
+ A completed check prints the result and exit code, coverage, findings, the OSV
66
+ advisories for that exact version, the AI status and next steps. The AI summary
67
+ appears only when the model actually returned one; otherwise the line names the
68
+ status, such as `unavailable (quota)`, and nothing stands in for it.
69
+
70
+ ```text
71
+ UNMASK fixture@1.0.0
72
+ Result Deterministic findings need review (exit 3)
73
+ Coverage complete: 1 file, 81 B
74
+ 0 byte-identical, 1 changed, 0 artifact-only, 0 not-compared
75
+ Source selected via user-full-SHA at dddddddddddd
76
+ Findings 1
77
+ - lifecycle-source-absent (script install)
78
+ Advisory none reported by OSV for this exact version
79
+ AI complete, 0 hypotheses (separate from deterministic evidence)
80
+ Summary: The artifact declares an install script that the selected source does not.
81
+ Scope: 1 evidence items supplied, 0 omitted; not checked: runtime-behavior
82
+ Uncertainty: The script was not executed, so its runtime effect is unknown.
83
+ Next
84
+ - Review each finding and its cited evidence before installing.
85
+ - AI suggests: Compare the install script with the upstream repository before installing.
86
+ - Add --out DIR to save the full JSON and HTML evidence.
87
+ ```
88
+
89
+ When the registry artifact cannot be analyzed, for example a removed release or
90
+ a timed-out run, the check still shows the advisories for that exact version. It
91
+ says that nothing was inspected and exits `2`:
92
+
93
+ ```text
94
+ UNMASK ua-parser-js@0.7.29
95
+ Result Advisory-only: artifact not inspected; no verdict is claimed (exit 2)
96
+ Artifact not inspected (execution-timeout); source comparison not performed
97
+ Advisory 1 known for this exact version (OSV)
98
+ - GHSA-pjwm-rvh2-c87w Embedded malware in ua-parser-js
99
+ AI not run; no artifact evidence to explain
100
+ ```
101
+
102
+ Progress and errors go to stderr, so stdout stays clean for pipes. Color appears
103
+ only on an interactive terminal; set `NO_COLOR=1` to turn it off. Exit `0` means
104
+ no review rule fired in the inspected scope. It does not mean safe, and a
105
+ missing advisory is not evidence of safety.
4
106
 
5
- UNMASK compares an npm package's published bytes with a selected source archive.
6
- It produces a JSON report and an HTML evidence viewer: matching files, changed
7
- bytes, artifact-only files, uninspected content, and observations to review.
8
- A completed report is not a safety verdict.
107
+ ## What a check returns
9
108
 
10
- Built for **Problem 04: Published Package vs Source Code Integrity Analyzer** at
11
- Hacktoberfest 2026, Hack Day Sonipat. See [HACKATHON.md](HACKATHON.md).
109
+ A real report for **`zod@4.6.7`**, from the live service:
12
110
 
13
- ## Status
111
+ | What UNMASK found | Result |
112
+ | ---------------------------------------- | ------------------------------------------------ |
113
+ | Published files compared with the source | **840**, against the commit the package claims |
114
+ | Byte-identical to the source | **335** |
115
+ | In the package but not in the source | **505**, mostly build output, each one listed |
116
+ | AI explanations | **3**, every quotation checked against the bytes |
117
+ | Policy result | Exit `0`: no review rule fired. Never "safe" |
14
118
 
15
- The local candidate includes an npm CLI, a shared analyzer, a website with a
16
- bounded job queue, and an optional hosted Gemma adapter. It uses plain ESM
17
- JavaScript on Node.js 24.22.2. The npm package is `@xagents_in/unmask`.
18
- Release `0.1.0` was published and verified; the current source candidate is
19
- `0.1.1`, whose release and deployment acceptance are pending. The default service is
20
- `https://unmask-386156642190.asia-south1.run.app`. See the dated
21
- [deployment evidence](docs/README.md) for observed runtime versions and commits;
22
- source configuration does not establish acceptance of a new release.
119
+ Alongside the report, `/r/<id>.details.json` gives a package profile: declared
120
+ entry points, direct dependencies, install scripts, static capabilities,
121
+ provenance, coverage, and the [OSV](https://osv.dev) advisories for that exact
122
+ version. This view is merged on `main` but not yet on the live service.
23
123
 
24
- The analyzer reads package contents as data. It does not install the inspected
25
- package, run its scripts, follow archive links, or reproduce its build.
26
- Source selection is a claim about correspondence, not proof of publisher trust.
124
+ Every file lands in exactly one state, and every claim points to a byte range in a
125
+ hashed file:
27
126
 
28
- ## Run locally
127
+ | State | Meaning |
128
+ | ---------------- | ------------------------------------------------------------- |
129
+ | `byte-identical` | Same size and SHA-256 as its source file |
130
+ | `changed` | A source twin exists, the bytes differ, and the span is shown |
131
+ | `artifact-only` | In the package, nowhere in the selected source |
132
+ | `not-compared` | UNMASK could not judge, and says why. Nothing is hidden |
133
+
134
+ ## How it works
135
+
136
+ ```mermaid
137
+ flowchart TD
138
+ A["Exact release<br/>name@version"] --> B["Fetch the npm tarball<br/>and the claimed commit"]
139
+ B --> C["Unpack both as data<br/>nothing is run"]
140
+ C --> D["Compare every file<br/>byte for byte"]
141
+ D --> E["Read manifests<br/>and syntax cues"]
142
+ E --> F["Apply fixed rules<br/>exit 0 / 2 / 3"]
143
+ F --> G["Report: JSON and<br/>escaped HTML"]
144
+ F -. "only if something differs" .-> H["AI explains the evidence<br/>quotes checked"]
145
+ H -.-> G
146
+ ```
147
+
148
+ 1. **Name an exact release.** Tags, ranges and URLs are refused, so every result can
149
+ be reproduced.
150
+ 2. **Fetch both, run neither.** The tarball comes from `registry.npmjs.org` and the
151
+ source from GitHub at the full commit the package claims. No install script,
152
+ build or test ever runs.
153
+ 3. **Evidence for every difference.** Each published file is set beside its source
154
+ twin. The verdict comes from fixed rules, before any AI is involved.
155
+
156
+ | Exit | Meaning |
157
+ | ---- | ------------------------------------------------------------------------- |
158
+ | `0` | Analysis completed and no review rule fired. This is not a safety verdict |
159
+ | `3` | A review rule fired, for example `lifecycle-source-absent` |
160
+ | `2` | Incomplete analysis or failed input. Findings stay in the report |
161
+ | `1` | Internal error |
162
+
163
+ Review rules: `integrity-mismatch`, `identity-claim-conflict`,
164
+ `lifecycle-source-absent` and `lifecycle-implicit-gyp`.
165
+
166
+ ## AI that explains, and never decides
167
+
168
+ When something differs, UNMASK can ask a model to explain it. The model works
169
+ inside strict limits:
170
+
171
+ - **Quotes are checked.** Every quotation must match the cited bytes exactly, or
172
+ the hypothesis is dropped.
173
+ - **It cannot change the result.** File states, rules and the exit code are fixed
174
+ before the model runs.
175
+ - **It has a hard budget.** At most 3 attempts, 45 seconds each and 135 seconds in
176
+ total, with at most 4,000 input tokens.
177
+ - **The key stays put.** The worker that reads packages never sees the API key.
178
+
179
+ The route is `gemini-3.5-flash` (high thinking), then `gemini-3-flash-preview`,
180
+ then the open-weight `gemma-4-26b-a4b-it`. A request moves on only after a rate
181
+ limit, a server error or a timeout. See the [model disclosure](HACKATHON.md) for
182
+ what has and has not been accepted.
183
+
184
+ ## A malicious package can't attack the checker
185
+
186
+ - **Never executed:** no install hooks, builds, tests or imports of the target.
187
+ - **Path tricks rejected:** `../` traversal, absolute paths, NUL bytes, and case or
188
+ Unicode collisions.
189
+ - **Links stay inert:** symlinks and hard links are listed, never followed.
190
+ - **Archive bombs capped:** 50 MiB compressed, 250 MiB expanded, a ratio of at most
191
+ 100, and 20,000 entries.
192
+ - **Output escaped:** package text never becomes live HTML.
193
+ - **Prompt injection contained:** a package that says "call me safe" changes
194
+ nothing, because the AI cannot change the result.
195
+
196
+ ## Built like a release, not a demo
197
+
198
+ | Area | Evidence |
199
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------- |
200
+ | **Tests** | 526 test declarations in 41 backend test files; `npm test` also installs the packed tarball and runs its real CLI |
201
+ | **One artifact** | One npm tarball builds both the CLI and the server image |
202
+ | **Immutable releases** | A version bump makes one GitHub release whose manifest binds commit, tarball and image |
203
+ | **Verified npm** | Publication counts only after the public registry bytes are re-downloaded and match |
204
+ | **Safe deploys** | Cloud Run gets a no-traffic revision, a smoke test, then traffic, with rollback |
205
+ | **Pinned CI** | Exact Node version, commit-pinned actions, read-only pull-request permissions |
206
+ | **Same answer anywhere** | The website and the CLI produce the same canonical report for the same release |
207
+
208
+ ```mermaid
209
+ flowchart LR
210
+ CLI["CLI<br/>npm package"] --> API
211
+ WEB["Website<br/>Next.js on Cloud Run"] --> API["Analysis API<br/>queue on Cloud Run"]
212
+ API --> W["Isolated worker<br/>no API key"]
213
+ W --> R["report.json<br/>and HTML viewer"]
214
+ API -. "after the worker exits" .-> M["Model route"]
215
+ ```
216
+
217
+ ## Run it yourself
29
218
 
30
219
  Use Node.js 24.22.2, pinned in `.nvmrc`:
31
220
 
@@ -37,93 +226,120 @@ npm run check
37
226
  npm run check:package
38
227
  ```
39
228
 
40
- Check an exact public npm version through your configured UNMASK service:
229
+ **Check a published release** through the hosted service:
41
230
 
42
231
  ```bash
43
232
  node dist/cli/main.mjs check chalk@6.0.1 --out /tmp/unmask-chalk-report
44
233
  ```
45
234
 
46
- Choose a fresh output directory for each check. The command writes `report.json`
47
- and `report.html` and refuses to replace existing report files. Public-package
48
- checks send only the exact package spec to the hosted service; the service owns
49
- acquisition and model policy. `UNMASK_SERVER_URL` overrides the default service.
50
- It must be an HTTPS origin, or a loopback HTTP origin for local testing. Hosted
51
- failure never falls back to local acquisition.
52
- See [CLI configuration and data flow](docs/cli.md).
235
+ `--out` is optional. When given, choose a fresh output folder for each check;
236
+ existing report files are never replaced. `UNMASK_SERVER_URL` selects another service: an HTTPS origin, or a
237
+ loopback HTTP origin for local testing. A hosted failure never falls back to local
238
+ acquisition. See [CLI configuration and data flow](docs/cli.md).
53
239
 
54
- To analyze local archives without network acquisition:
240
+ **Check a local tarball offline**, for example a copy you suspect was tampered with:
55
241
 
56
242
  ```bash
57
243
  node dist/cli/main.mjs check --offline --tarball /path/to/package.tgz --source-archive /path/to/source.tar.gz --commit FULL_40_CHARACTER_LOWERCASE_COMMIT_SHA --source-root ARCHIVE_TOP_LEVEL_DIRECTORY --source-dir '' --cache-dir /tmp/unmask-local-cache --out /tmp/unmask-local-report
58
244
  ```
59
245
 
60
- `--source-dir ''` explicitly selects the repository root. For a monorepo, supply
61
- the package's directory relative to that root. Omit all source options for an
62
- artifact-only report. An optional `--claim name@version` checks an explicit
63
- identity claim against the artifact manifest.
246
+ `--source-dir ''` selects the repository root; for a monorepo, give the package's
247
+ folder instead. Leave out all source options for an artifact-only report.
248
+ `--claim name@version` checks an identity claim against the package manifest.
249
+ Excerpts leave the machine only if you add `--allow-model` and configure a provider.
250
+
251
+ **Commands in this build.** `node dist/cli/main.mjs --help` (or `unmask --help`
252
+ once installed) prints the same list, and `unmask help COMMAND` shows its usage.
64
253
 
65
- Exit codes: `0` means analysis completed; `2` means analysis was incomplete or
66
- input acquisition failed; `3` means a default review-policy finding; `1` means an
67
- internal error. Findings remain visible when incomplete analysis takes priority.
68
- Exit `0` never means a package is safe.
254
+ | Command | What it does |
255
+ | ------------- | ------------------------------------------------------------------------------ |
256
+ | `check` | Check one exact version through the hosted service, or a local tarball offline |
257
+ | `install` | Refuse mutable npm install and explain the lockfile-first ci path |
258
+ | `ci` | Screen the exact lockfile plan before invoking npm ci |
259
+ | `enable` | Print explicit reversible shell integration |
260
+ | `disable` | Print how to remove the shell integration |
261
+ | `audit` | Check exact versions locked in a package-lock.json, with bounded coverage |
262
+ | `why` | Explain dependency paths recorded in the lockfile |
263
+ | `info` | Show hosted package metadata, scripts, dependencies and recorded assessment |
264
+ | `files` | List a bounded hosted package file inventory |
265
+ | `file` | Inspect recorded file evidence using a report-bound file selector |
266
+ | `compare` | Compare scripts, dependencies and files between two exact versions |
267
+ | `history` | Show a bounded registry release timeline with exact-version guidance |
268
+ | `config` | Manage project-local guard policy settings |
269
+ | `exceptions` | Manage explicit expiring exact-version guard exceptions |
270
+ | `init` | Create project guard configuration explicitly |
271
+ | `doctor` | Diagnose Node, service reachability and project guard configuration |
272
+ | `cache` | Import, inspect and explicitly prune local report snapshots |
273
+ | `sarif` | Export recorded findings as SARIF from an existing report |
274
+ | `annotations` | Produce escaped CI annotations and a summary from an existing report |
275
+ | `watch` | Opt in to watch a lockfile and check changes |
276
+ | `preflight` | Check changed lockfile entries against a baseline for CI |
277
+ | `scan` | Resolve a dist-tag to an exact version, then check it |
278
+ | `batch` | Check several explicit exact versions serially with duplicate coalescing |
69
279
 
70
- Start the website locally:
280
+ The explicit install guard can block before npm when policy or incomplete evidence
281
+ requires it. Inspection commands report recorded evidence and coverage; exit `0`
282
+ never means a package is safe.
283
+
284
+ **Run the website locally:**
71
285
 
72
286
  ```bash
73
287
  UNMASK_MODEL=off HOST=127.0.0.1 PORT=3000 node dist/server/main.mjs
74
288
  ```
75
289
 
76
- Visit `http://127.0.0.1:3000`. The website accepts exact public npm versions,
77
- queues analysis, and serves the same report format as the CLI. It accepts no
78
- uploads. Its development worker limits and temporary-file cleanup do not
79
- constitute an operating-system security sandbox.
80
-
81
- The Next.js frontend in [`web/`](web/README.md) can sit in front of that server.
82
- Start it with `UNMASK_API_URL=http://127.0.0.1:3000`; see its README.
83
-
84
- ## Inspection limits
85
-
86
- - File correspondence uses hashes and bounded byte comparisons. Transpilation,
87
- minification, sourcemaps and declared output locations do not prove equivalence.
88
- - Syntax cues use a bounded JavaScript parser. TypeScript, JSX, unsupported syntax,
89
- dynamic behavior and unretained content remain uninspected.
90
- - Literal novelty means only "not found in inspected source". Missing or
91
- insufficient source cannot establish full-source absence.
92
- - Ambiguous source-package selection is incomplete. For example, multiple
93
- same-name manifests in Axios source require an explicit local source directory;
94
- the website currently has no such override.
95
- - This candidate covers npm only. PyPI and continuous release monitoring are
96
- not implemented.
97
-
98
- ## Models and releases
99
-
100
- Model output is advisory and cannot change deterministic policy findings. The
101
- adapter has recorded-transport tests, but real endpoint availability, account
102
- quotas and usefulness still require validation. Hosted CLI checks follow the
103
- service's published model/data disclosure and never read a local provider key.
104
- Only offline local checks use `--allow-model`; without that flag no local model
105
- client is constructed. The flag alone does not configure an offline provider.
106
- See [HACKATHON.md](HACKATHON.md) for the exact candidate and remaining AI gate.
107
-
108
- `npm test` runs product and release-tooling tests, including a packed tarball
109
- installed into a temporary directory and invoked through its real CLI binary.
110
- `npm run check` checks formatting; it is not static type checking. Product tests
111
- and local HTTP checks do not establish deployment readiness.
112
-
113
- Release publication is enabled for the `0.1.1` source candidate, subject to CI
114
- and runtime release gates. A completed immutable release with verified npm
115
- publication triggers API deployment in the same workflow. Successful API
116
- verification then triggers website deployment for that exact release commit.
117
- See [release prerequisites](docs/releases.md). npm access is not required for
118
- local builds or testing.
290
+ Then visit `http://127.0.0.1:3000`. The Next.js frontend in [`web/`](web/README.md)
291
+ can sit in front of it with `UNMASK_API_URL=http://127.0.0.1:3000`.
292
+
293
+ ## What UNMASK does not claim
294
+
295
+ Being clear about limits is part of the product.
296
+
297
+ - **Matching code can still be malicious.** If the bad code is in the source too,
298
+ a comparison cannot catch it.
299
+ - **Build output is noisy.** Bundled and transpiled files have no source twin;
300
+ sourcemaps and declared output locations do not prove equivalence.
301
+ - **Syntax cues are bounded.** TypeScript, JSX, unsupported syntax and dynamic
302
+ behaviour stay uninspected. "Novel" means only "not found in the inspected
303
+ source".
304
+ - **Ambiguous sources stay incomplete.** If a repository has two manifests with the
305
+ package's name (axios does), the CLI needs `--source-dir`; the website has no
306
+ override yet.
307
+ - **The AI is advisory.** A checked quotation proves where the model looked, not
308
+ that its reasoning is right. Open-weight acceptance of the Gemma tier is still
309
+ pending.
310
+ - **npm only, for now.** PyPI and continuous release monitoring are not
311
+ implemented.
312
+
313
+ ## Read more
314
+
315
+ | Page | What it covers |
316
+ | ---------------------------------------------------- | --------------------------------------------------------- |
317
+ | [HACKATHON.md](HACKATHON.md) | Team, problem statement and the exact AI model disclosure |
318
+ | [Engineering docs](docs/README.md) | Live services, model evidence and the documentation map |
319
+ | [Architecture](docs/architecture.md) | Flows, process boundaries and job states |
320
+ | [Contracts](docs/contracts.md) | Inputs, report shape, exit policy and HTTP behaviour |
321
+ | [CLI](docs/cli.md) | Hosted and offline CLI configuration and data flow |
322
+ | [Releases](docs/releases.md) | Release gates and artifact verification |
323
+ | [Cloud Run deployment](docs/cloud-run-deployment.md) | How a release reaches production |
324
+ | [Website](web/README.md) | The Next.js site and its connection to the analyzer |
325
+
326
+ ## Team Cappybara
327
+
328
+ Built in one night at **Hacktoberfest 2026, Hack Day Sonipat**, for
329
+ **Problem 04: Published Package vs Source Code Integrity Analyzer**.
330
+
331
+ | Member | Focus |
332
+ | ------------------ | ---------------------------------------------- |
333
+ | Pranay Vishwakarma | Team lead, website |
334
+ | Vaiditya Tanwar | Analyzer, AI route, CI, release and deployment |
335
+ | Vishesh Rao | CLI and npm release |
336
+ | Sayan Bhattacharya | Project report, QA and documentation |
119
337
 
120
338
  ## Contributing
121
339
 
122
340
  See [CONTRIBUTING.md](CONTRIBUTING.md) for review, test and comment conventions.
123
- Every remote change after the initial event commit goes through a PR. Repository
124
- permissions and code-owner enforcement must be verified on GitHub; the
125
- CODEOWNERS file alone does not enforce them.
341
+ Every change after the initial event commit goes through a pull request.
126
342
 
127
343
  ## License
128
344
 
129
- [MIT](LICENSE).
345
+ [MIT](LICENSE)
@@ -0,0 +1,145 @@
1
+ import { constants } from "node:fs";
2
+ import { open } from "node:fs/promises";
3
+ import { LocalInputError } from "../core/pipeline.mjs";
4
+ import { collectFindings } from "../core/sarif.mjs";
5
+ import { parseExportOptions, readExportInputs } from "./sarif.mjs";
6
+
7
+ const MAX_SUMMARY_ITEMS = 50;
8
+ const commands = { error: "error", warning: "warning", note: "notice" };
9
+ const visible = (value) =>
10
+ String(value).replace(
11
+ /[\u0000-\u0009\u000b\u000c\u000e-\u001f\u007f-\u009f\u2028\u2029]/g,
12
+ (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`,
13
+ );
14
+
15
+ export function escapeData(value) {
16
+ return visible(value)
17
+ .replace(/%/g, "%25")
18
+ .replace(/\r/g, "%0D")
19
+ .replace(/\n/g, "%0A");
20
+ }
21
+
22
+ export function escapeProperty(value) {
23
+ return escapeData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
24
+ }
25
+
26
+ export function escapeMarkdown(value) {
27
+ return visible(value)
28
+ .replace(/[\r\n]+/g, " ")
29
+ .replace(/[\\`*_{}[\]()<>#+!|~&"'=-]/g, (char) => `\\${char}`);
30
+ }
31
+
32
+ function where(result) {
33
+ const spans = result.locations.map(
34
+ (item) =>
35
+ `${item.side} ${item.path} bytes ${item.byteOffset}-${item.byteOffset + item.byteLength}`,
36
+ );
37
+ return spans.length ? ` [${spans.join("; ")}]` : "";
38
+ }
39
+
40
+ export function annotationOutcome(collected) {
41
+ const { report, advisories, results } = collected;
42
+ let exitCode = report.policy.exitCode;
43
+ const knownAdvisory = results.some((item) =>
44
+ ["advisory/known-malware", "advisory/known-vulnerability"].includes(
45
+ item.ruleId,
46
+ ),
47
+ );
48
+ if (exitCode === 0 && knownAdvisory) exitCode = 3;
49
+ if (exitCode === 0 && advisories && !advisories.complete) exitCode = 2;
50
+ return exitCode;
51
+ }
52
+
53
+ export function renderAnnotations(collected) {
54
+ const lines = [];
55
+ for (const result of collected.results)
56
+ lines.push(
57
+ `::${commands[result.level]} title=${escapeProperty(`unmask ${result.ruleId}`)}::${escapeData(result.message + where(result))}`,
58
+ );
59
+ for (const item of collected.limitations)
60
+ lines.push(
61
+ `::${commands[item.level]} title=${escapeProperty(`unmask ${item.id}`)}::${escapeData(item.message)}`,
62
+ );
63
+ return lines.join("\n") + "\n";
64
+ }
65
+
66
+ export function renderSummary(collected, exitCode) {
67
+ const { report, identity, advisories, results, limitations } = collected;
68
+ const conflict = identity?.identityStatus === "claim-conflicts-with-manifest";
69
+ const name = identity
70
+ ? conflict
71
+ ? `${identity.name}@${identity.version} (claimed; artifact declares ${identity.declared.name}@${identity.declared.version})`
72
+ : `${identity.name}@${identity.version}`
73
+ : "unidentified artifact";
74
+ const count = (level) =>
75
+ results.filter((item) => item.level === level).length;
76
+ const lines = [
77
+ `## UNMASK check: ${escapeMarkdown(name)}`,
78
+ "",
79
+ "| Item | Value |",
80
+ "| --- | --- |",
81
+ `| Exit code | ${exitCode} |`,
82
+ `| Policy findings | ${report.policy.findings.length} |`,
83
+ `| Coverage complete | ${report.coverage.complete ? "yes" : "no"} |`,
84
+ `| Identity | ${escapeMarkdown(identity?.identityStatus ?? "unknown")} |`,
85
+ `| Advisory check | ${escapeMarkdown(advisories ? `${advisories.status}${advisories.boundTo === "claim" && identity.identityStatus !== "claim-matches-manifest" ? " (for requested identity)" : ""}` : "not supplied")} |`,
86
+ `| Results | ${count("error")} error, ${count("warning")} warning, ${count("note")} note |`,
87
+ `| Artifact sha256 | \`${report.inputs.artifact.sha256}\` |`,
88
+ "",
89
+ ];
90
+ if (results.length) {
91
+ lines.push("### Results", "");
92
+ for (const item of results.slice(0, MAX_SUMMARY_ITEMS))
93
+ lines.push(
94
+ `- **${item.level}** \`${item.ruleId}\`: ${escapeMarkdown(item.message + where(item))}`,
95
+ );
96
+ if (results.length > MAX_SUMMARY_ITEMS)
97
+ lines.push(
98
+ `- ${results.length - MAX_SUMMARY_ITEMS} more results omitted`,
99
+ );
100
+ lines.push("");
101
+ }
102
+ lines.push("### Limitations", "");
103
+ for (const item of limitations)
104
+ lines.push(`- ${item.level}: ${escapeMarkdown(item.message)}`);
105
+ return lines.join("\n") + "\n\n";
106
+ }
107
+
108
+ async function appendSummary(filename, markdown) {
109
+ let handle;
110
+ try {
111
+ handle = await open(
112
+ filename,
113
+ constants.O_WRONLY |
114
+ constants.O_APPEND |
115
+ constants.O_CREAT |
116
+ constants.O_NONBLOCK |
117
+ (constants.O_NOFOLLOW ?? 0),
118
+ 0o600,
119
+ );
120
+ if (!(await handle.stat()).isFile()) throw new Error("not regular");
121
+ await handle.writeFile(markdown);
122
+ } catch {
123
+ throw new LocalInputError(
124
+ "output-write-failed",
125
+ "Summary requires a regular, writable file path",
126
+ );
127
+ } finally {
128
+ await handle?.close();
129
+ }
130
+ }
131
+
132
+ export async function annotationsCommand(argv, stdout = process.stdout) {
133
+ const options = parseExportOptions(argv, {
134
+ "--report": "report",
135
+ "--details": "details",
136
+ "--summary": "summary",
137
+ });
138
+ const { report, details } = await readExportInputs(options);
139
+ const collected = collectFindings(report, details);
140
+ const exitCode = annotationOutcome(collected);
141
+ if (options.summary !== undefined)
142
+ await appendSummary(options.summary, renderSummary(collected, exitCode));
143
+ stdout.write(renderAnnotations(collected));
144
+ return exitCode;
145
+ }