residoo 0.3.4 → 0.3.6

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/README.md CHANGED
@@ -58,11 +58,13 @@ precise about rather than lumping together:
58
58
  do nothing for the months of transcripts already sitting on disk, or for
59
59
  any session run without the hook active. residoo scans **retroactively, at
60
60
  rest**: every file already there, from every past session.
61
- - **agentsweep** is a genuine, welcome peer covering similar ground. Broader,
62
- in fact: 31 agent sources and 209 detection rules to residoo's smaller set,
63
- plus in-place redaction, SARIF output, and a pre-commit hook. The tradeoffs
64
- are worth naming precisely rather than either dismissing it or copying it
65
- blindly. It needs Python 3.11+ and three pip packages (all clean ones, on
61
+ - **agentsweep** is a genuine, welcome peer covering similar ground. Broader
62
+ on detection rules (209 to residoo's smaller, deliberately high-confidence
63
+ set) and it does in-place redaction, where residoo's `--seal` makes an
64
+ encrypted copy instead. residoo has more agent sources (43 to 31), and
65
+ both now ship SARIF output and a pre-commit hook. The tradeoffs are worth
66
+ naming precisely rather than either dismissing it or copying it blindly.
67
+ It needs Python 3.11+ and three pip packages (all clean ones, on
66
68
  inspection; no known CVEs), where residoo needs nothing beyond Node. Its
67
69
  own README documents that its in-place redaction leaves the pre-redaction
68
70
  original sitting in a **plaintext** `.bak` file, and its issue tracker shows
@@ -119,6 +121,17 @@ won't be built into the tool that writes it.
119
121
  - Redacts everything in its own output. You get a shape and a first/last-4
120
122
  preview, never the real value, including in `--json` mode. A decoded or
121
123
  rejoined secret is redacted exactly like a plain one.
124
+ - `--sarif` emits SARIF 2.1.0 for GitHub code scanning's Security tab and
125
+ inline pull-request annotations, the same format gitleaks/trufflehog/
126
+ agentsweep already speak, so residoo's own Action and pre-commit hook plug
127
+ straight into GitHub's native UI. `--json` remains the format for the full
128
+ picture (findings, integrity, rotation) together.
129
+ - `--seal --keychain` stores the vault key in the OS's own secure credential
130
+ store (macOS today, Linux with `secret-tool` installed) instead of a typed
131
+ passphrase: nothing to remember, and a truly random key instead of one
132
+ whose strength depends on what you typed. Tradeoff stated plainly: a
133
+ keychain-backed vault lives on that machine/account only, a passphrase
134
+ travels, a keychain-backed key does not. See `src/keychain.js`.
122
135
  - Tells you how many **distinct** secrets it found versus how many times one
123
136
  got echoed back across tool calls, so the headline number reflects real
124
137
  exposure, not repetition.
@@ -304,7 +317,7 @@ As a GitHub Action (this repository doubles as a composite action):
304
317
  ```yaml
305
318
  steps:
306
319
  - uses: actions/checkout@v4
307
- - uses: dandovdub/residoo@v0.3.4
320
+ - uses: dandovdub/residoo@v0.3.6
308
321
  ```
309
322
 
310
323
  As a pre-commit hook:
@@ -312,12 +325,15 @@ As a pre-commit hook:
312
325
  ```yaml
313
326
  repos:
314
327
  - repo: https://github.com/dandovdub/residoo
315
- rev: v0.3.4
328
+ rev: v0.3.6
316
329
  hooks:
317
330
  - id: residoo
318
331
  ```
319
332
 
320
- Or with no integration at all: `npx --yes residoo scan --project . --fail-on-find`.
333
+ Or with no integration at all: `npm install -g residoo && residoo scan --project . --fail-on-find`
334
+ (more reliable in CI than `npx --yes`, which failed consistently in real
335
+ GitHub Actions runs while working fine locally; see the design note at the
336
+ top of `action.yml`).
321
337
  Exit codes, inputs, and exactly what project mode does and does not see are
322
338
  documented in [docs/ci.md](docs/ci.md).
323
339
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "residoo",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Find secrets leaking through your AI coding agent's session history. Zero network calls in the scan path, zero dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "CloudRoam (https://cloudroam.io)",
package/src/cli.js CHANGED
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
 
3
3
  const path = require("path");
4
+ const fs = require("fs");
5
+ const crypto = require("crypto");
4
6
  const { availableSources, ALL_SOURCES } = require("./sources");
5
7
  const { scan, emptyResult } = require("./scan");
6
- const { render, renderIntegrity, renderJson } = require("./report");
8
+ const { render, renderIntegrity, renderJson, renderSarif } = require("./report");
7
9
  const { checkIntegrity } = require("./integrity");
8
10
  const {
9
11
  ROTATION_GUIDANCE, guidanceFor, loadAcks, ackFinding, renderRotation,
@@ -62,6 +64,10 @@ Usage:
62
64
 
63
65
  Scan options:
64
66
  --json machine-readable output (full detail, still redacted)
67
+ --sarif SARIF 2.1.0 output (secret findings only), for
68
+ GitHub code scanning's Security tab and inline PR
69
+ annotations. Use --json for the full picture
70
+ (findings + integrity + rotation) instead.
65
71
  --project [dir] scan a repository checkout instead of this machine
66
72
  (default dir: current directory). Covers committed
67
73
  agent transcripts, agent config/rules files, and
@@ -101,6 +107,14 @@ Seal options (used with scan):
101
107
  --seal after scanning, encrypt every transcript that carried a
102
108
  finding into a local vault directory (AES-256-GCM,
103
109
  passphrase-derived key; originals are left untouched)
110
+ --keychain with --seal (or unseal): use a truly random key stored
111
+ in the OS keychain instead of a typed passphrase.
112
+ Nothing to remember, and the key's strength no longer
113
+ depends on passphrase choice. macOS today; Linux when
114
+ secret-tool (libsecret) is installed. TRADEOFF: a
115
+ keychain-backed vault lives on THIS machine/account
116
+ only, unlike a passphrase, it is not portable to
117
+ another machine.
104
118
  --vault-dir <dir> where to create the vault (default: ./residoo-vault-<stamp>)
105
119
  --upload-cloudroam ALSO upload the sealed vault to CloudRoam. This is the
106
120
  only residoo feature that touches the network, it is
@@ -116,6 +130,10 @@ Unseal:
116
130
  restore one entry, verified
117
131
  byte-identical via its
118
132
  recorded SHA-256
133
+ --keychain add to either unseal form above: retrieve the vault
134
+ key from the OS keychain instead of prompting for a
135
+ passphrase. Only works for a vault that was sealed
136
+ with --keychain on this same machine/account.
119
137
 
120
138
  The passphrase is read from RESIDOO_PASSPHRASE, or prompted (hidden) on a TTY.
121
139
 
@@ -138,6 +156,52 @@ async function getPassphrase({ confirmNew }) {
138
156
  return p1;
139
157
  }
140
158
 
159
+ /**
160
+ * The sealing secret for `scan --seal`: a keychain-generated random key (see
161
+ * keychain.js), or a typed passphrase. `vaultId` is null in passphrase mode;
162
+ * in keychain mode the caller writes it to `.keychain-id` inside the vault
163
+ * once sealFindings has created the directory, so unseal can find it again.
164
+ * The generated secret is passed straight through to the SAME
165
+ * sealFindings/deriveKey path a typed passphrase would use — scrypt on a
166
+ * full 256-bit-entropy input is harmless extra defense, and reusing that
167
+ * already-tested path means no change to sealcrypto.js/sealvault.js at all.
168
+ */
169
+ async function resolveSealSecret(args) {
170
+ if (!args.includes("--keychain")) {
171
+ return { passphrase: await getPassphrase({ confirmNew: true }), vaultId: null };
172
+ }
173
+ const keychain = require("./keychain");
174
+ if (!keychain.isSupported()) throw new Error(`--keychain: ${keychain.unsupportedReason()}`);
175
+ const vaultId = crypto.randomUUID();
176
+ const passphrase = crypto.randomBytes(32).toString("base64");
177
+ keychain.store(vaultId, passphrase);
178
+ return { passphrase, vaultId };
179
+ }
180
+
181
+ /** The unsealing secret for `unseal`: a keychain-retrieved key, or a typed passphrase. */
182
+ async function resolveUnsealSecret(args, vaultDir) {
183
+ if (!args.includes("--keychain")) return getPassphrase({ confirmNew: false });
184
+ const keychain = require("./keychain");
185
+ if (!keychain.isSupported()) throw new Error(`--keychain: ${keychain.unsupportedReason()}`);
186
+ const idPath = path.join(vaultDir, ".keychain-id");
187
+ if (!fs.existsSync(idPath)) {
188
+ throw new Error(
189
+ `No .keychain-id marker in ${vaultDir}: this vault was not sealed with --keychain, ` +
190
+ `or the marker file was moved separately from the vault. Try unsealing without --keychain.`
191
+ );
192
+ }
193
+ const vaultId = fs.readFileSync(idPath, "utf-8").trim();
194
+ try {
195
+ return keychain.retrieve(vaultId);
196
+ } catch {
197
+ throw new Error(
198
+ "Could not retrieve this vault's key from the OS keychain. It may have been removed, " +
199
+ "or this may be a different machine/account than the one that sealed it: a keychain-backed " +
200
+ "vault is not portable across machines."
201
+ );
202
+ }
203
+ }
204
+
141
205
  async function runSeal(result, args) {
142
206
  const { sealFindings, uploadVaultToCloudRoam } = require("./sealvault");
143
207
 
@@ -149,21 +213,27 @@ async function runSeal(result, args) {
149
213
 
150
214
  const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
151
215
  const vaultDir = argValue(args, "--vault-dir") || path.resolve(`residoo-vault-${stamp}`);
152
- const passphrase = await getPassphrase({ confirmNew: true });
216
+ const { passphrase, vaultId } = await resolveSealSecret(args);
153
217
 
154
218
  process.stdout.write(`\nSealing ${filesWithFindings.length} file(s) with findings into ${vaultDir}\n`);
155
219
  const { entries } = await sealFindings({
156
220
  files: filesWithFindings, vaultDir, passphrase,
157
221
  log: (s) => process.stdout.write(s + "\n"),
158
222
  });
223
+ // Written only after sealFindings has created vaultDir. Plaintext, but
224
+ // holds nothing sensitive: a random id with no meaning outside this
225
+ // keychain lookup, never the key itself and never anything about what the
226
+ // vault contains.
227
+ if (vaultId) fs.writeFileSync(path.join(vaultDir, ".keychain-id"), vaultId, { mode: 0o600 });
159
228
  const totalPlain = entries.reduce((s, e) => s + e.plainBytes, 0);
160
229
  const totalSealed = entries.reduce((s, e) => s + e.sealedBytes, 0);
161
230
  process.stdout.write(
162
231
  `\nSealed ${entries.length} file(s): ${(totalPlain / 1024 / 1024).toFixed(1)}MB plain -> ` +
163
232
  `${(totalSealed / 1024 / 1024).toFixed(1)}MB encrypted.\n` +
164
233
  `Originals were NOT touched. Once you've verified a restore works\n` +
165
- `(residoo unseal ${path.basename(vaultDir)} --restore 0001.sealed --out /tmp/check), removing the\n` +
166
- `plaintext originals is your call; residoo never deletes anything itself.\n`
234
+ `(residoo unseal ${path.basename(vaultDir)}${vaultId ? " --keychain" : ""} --restore 0001.sealed --out /tmp/check), removing the\n` +
235
+ `plaintext originals is your call; residoo never deletes anything itself.\n` +
236
+ (vaultId ? `The vault key is stored in the OS keychain, never typed, never written in plaintext to disk.\n` : "")
167
237
  );
168
238
 
169
239
  if (args.includes("--upload-cloudroam")) {
@@ -192,7 +262,7 @@ async function runUnseal(args) {
192
262
  const vaultDir = args[1];
193
263
  if (!vaultDir) { process.stderr.write("usage: residoo unseal <vault-dir> [--restore <n> --out <path>]\n"); return 2; }
194
264
 
195
- const passphrase = await getPassphrase({ confirmNew: false });
265
+ const passphrase = await resolveUnsealSecret(args, vaultDir);
196
266
  let manifest;
197
267
  try {
198
268
  manifest = openManifest(vaultDir, passphrase);
@@ -314,6 +384,7 @@ async function main(argv) {
314
384
  }
315
385
 
316
386
  const wantsJson = args.includes("--json");
387
+ const wantsSarif = args.includes("--sarif");
317
388
  const includeNoisy = args.includes("--include-noisy");
318
389
  const includeSuppressed = args.includes("--include-suppressed");
319
390
  const failOnFind = args.includes("--fail-on-find");
@@ -402,7 +473,11 @@ async function main(argv) {
402
473
  if (sources.length === 0) {
403
474
  const empty = emptyResult();
404
475
  const integrity = wantsIntegrity ? runIntegrity() : null;
405
- if (wantsJson) {
476
+ if (wantsSarif) {
477
+ // Same contract as --json below: a CI step consuming SARIF must
478
+ // always get a valid SARIF document, even with nothing to scan.
479
+ process.stdout.write(renderSarif(empty) + "\n");
480
+ } else if (wantsJson) {
406
481
  // A --json caller (CI, a script piping into jq) must always get valid JSON
407
482
  // on stdout, even on the "nothing to scan" path — a plain-text message on
408
483
  // stderr with exit 0 silently breaks that contract.
@@ -422,9 +497,11 @@ async function main(argv) {
422
497
  const result = await scan({ sources, includeNoisy, includeSuppressed });
423
498
  const integrity = wantsIntegrity ? runIntegrity() : null;
424
499
  const rotation = renderRotation(result.findings, acks);
425
- process.stdout.write((wantsJson
426
- ? renderJson(result, integrity, rotation)
427
- : render(result, { noColor, integrity, rotation })) + "\n");
500
+ process.stdout.write((wantsSarif
501
+ ? renderSarif(result)
502
+ : wantsJson
503
+ ? renderJson(result, integrity, rotation)
504
+ : render(result, { noColor, integrity, rotation })) + "\n");
428
505
 
429
506
  if (args.includes("--seal")) {
430
507
  const sealExit = await runSeal(result, args);
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+
3
+ const { execFileSync } = require("child_process");
4
+
5
+ /**
6
+ * OS-native secure credential storage for `scan --seal --keychain` and
7
+ * `unseal --keychain`. Instead of a user-typed passphrase run through
8
+ * scrypt, residoo generates a truly random, high-entropy secret and hands
9
+ * it to the OS's own secure store: nothing needs to be typed or
10
+ * remembered, and the vault's strength no longer depends on a human's
11
+ * passphrase choice (the weak spot a security audit flagged in passphrase
12
+ * mode, where the only floor was an 8-character length check).
13
+ *
14
+ * The secret itself is still passed straight through to sealcrypto.js's
15
+ * existing deriveKey(passphrase, salt) exactly as a typed passphrase would
16
+ * be: scrypt on a full 256-bit-entropy input is harmless extra defense in
17
+ * depth, and reusing the same, already-tested code path here means no
18
+ * change to sealcrypto.js or sealvault.js at all — only how the secret is
19
+ * obtained changes.
20
+ *
21
+ * Scoped honestly rather than half-built everywhere: macOS via the
22
+ * `security` CLI (built into the OS, no new dependency) is the primary,
23
+ * fully-supported path. Linux via `secret-tool` (libsecret) works when it's
24
+ * installed and is treated as best-effort, gated by isSupported() the same
25
+ * way every source in this codebase declares availability rather than
26
+ * assuming it. Windows has no equivalent built-in CLI story and is refused
27
+ * outright with a clear message rather than half-implemented against a
28
+ * module residoo would have to newly depend on.
29
+ *
30
+ * IMPORTANT TRADEOFF, stated plainly: a keychain-backed vault key lives in
31
+ * THIS machine's (or account's) secure store. It is not portable the way a
32
+ * passphrase is — unseal it on a different machine and there is nothing to
33
+ * retrieve. Use a passphrase instead when a vault needs to travel.
34
+ */
35
+ const SERVICE = "residoo-vault";
36
+
37
+ /**
38
+ * Test-only escape hatch, macOS only: when RESIDOO_TEST_KEYCHAIN_FILE is
39
+ * set, every operation below scopes to that keychain FILE instead of the
40
+ * real default login keychain. Exists so this project's own tests can run
41
+ * a genuine store/retrieve/remove round trip against a throwaway keychain
42
+ * created and destroyed within the test (see tests/smoke.js), crossing a
43
+ * spawned child process boundary via env var rather than a function
44
+ * parameter, without ever touching, prompting about, or depending on
45
+ * whatever machine happens to run them. Not a documented flag: no real
46
+ * user has a reason to set this, and even if one did, the only effect is
47
+ * redirecting to a named file instead of the default keychain, never an
48
+ * unexpected access to anything.
49
+ */
50
+ function testKeychainFile() {
51
+ return process.env.RESIDOO_TEST_KEYCHAIN_FILE || null;
52
+ }
53
+
54
+ function isSupported() {
55
+ if (process.platform === "darwin") return true;
56
+ if (process.platform === "linux") {
57
+ try {
58
+ execFileSync("which", ["secret-tool"], { stdio: "ignore" });
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+
67
+ function unsupportedReason() {
68
+ if (process.platform === "darwin") return null;
69
+ if (process.platform === "linux") {
70
+ return "secret-tool (libsecret) is not installed. Install it (e.g. \"apt install libsecret-tools\" or \"dnf install libsecret\") or omit --keychain to use a passphrase instead.";
71
+ }
72
+ return `--keychain is not supported on ${process.platform} yet. Omit --keychain to use a passphrase instead.`;
73
+ }
74
+
75
+ /**
76
+ * Store `secret` (a string) under `account` in the OS keychain, for later
77
+ * retrieve(account). `keychainFile`, macOS only, is an escape hatch used
78
+ * ONLY by this project's own tests: passing a path scopes the operation to
79
+ * that specific keychain FILE instead of the real default login keychain,
80
+ * so a test round-trip never touches, prompts about, or depends on the
81
+ * developer's actual keychain. The real feature (seal/unseal) never passes
82
+ * this — it always targets the default keychain, which is the whole point.
83
+ */
84
+ function store(account, secret, keychainFile) {
85
+ if (process.platform === "darwin") {
86
+ // -U updates the entry in place if `account` already exists, rather
87
+ // than erroring on a name collision.
88
+ const kf = keychainFile || testKeychainFile();
89
+ const args = ["add-generic-password", "-a", account, "-s", SERVICE, "-w", secret, "-U"];
90
+ if (kf) args.push(kf);
91
+ execFileSync("security", args, { stdio: "ignore" });
92
+ return;
93
+ }
94
+ if (process.platform === "linux") {
95
+ // secret-tool reads the secret from stdin, never a CLI argument, so it
96
+ // never appears in a process listing or shell history.
97
+ execFileSync("secret-tool", [
98
+ "store", "--label", "residoo sealed vault key", "service", SERVICE, "account", account,
99
+ ], { input: secret, stdio: ["pipe", "ignore", "ignore"] });
100
+ return;
101
+ }
102
+ throw new Error(unsupportedReason());
103
+ }
104
+
105
+ /** Retrieve a secret previously stored under `account`. Throws if not found or unsupported. See store() re: keychainFile. */
106
+ function retrieve(account, keychainFile) {
107
+ if (process.platform === "darwin") {
108
+ const kf = keychainFile || testKeychainFile();
109
+ const args = ["find-generic-password", "-a", account, "-s", SERVICE, "-w"];
110
+ if (kf) args.push(kf);
111
+ return execFileSync("security", args, { encoding: "utf8" }).trim();
112
+ }
113
+ if (process.platform === "linux") {
114
+ return execFileSync("secret-tool", [
115
+ "lookup", "service", SERVICE, "account", account,
116
+ ], { encoding: "utf8" }).trim();
117
+ }
118
+ throw new Error(unsupportedReason());
119
+ }
120
+
121
+ /**
122
+ * Remove a previously stored secret. Not used by seal/unseal (a vault's
123
+ * keychain entry is meant to outlive the command that created it); exists
124
+ * for callers that manage a keychain entry's lifecycle themselves, and for
125
+ * this project's own tests to clean up after a real round-trip check
126
+ * without leaving entries behind. See store() re: keychainFile.
127
+ */
128
+ function remove(account, keychainFile) {
129
+ if (process.platform === "darwin") {
130
+ const kf = keychainFile || testKeychainFile();
131
+ const args = ["delete-generic-password", "-a", account, "-s", SERVICE];
132
+ if (kf) args.push(kf);
133
+ execFileSync("security", args, { stdio: "ignore" });
134
+ return;
135
+ }
136
+ if (process.platform === "linux") {
137
+ execFileSync("secret-tool", ["clear", "service", SERVICE, "account", account], { stdio: "ignore" });
138
+ return;
139
+ }
140
+ throw new Error(unsupportedReason());
141
+ }
142
+
143
+ module.exports = { isSupported, unsupportedReason, store, retrieve, remove };
package/src/report.js CHANGED
@@ -318,4 +318,75 @@ function renderJson(result, integrity = null, rotation = null) {
318
318
  );
319
319
  }
320
320
 
321
- module.exports = { render, renderIntegrity, renderRotationSection, renderJson };
321
+ // confidence -> SARIF level. "high"/"medium" map to the two levels GitHub's
322
+ // code-scanning UI treats as real alerts ("error" surfaces most
323
+ // prominently); "low" only ever appears with --include-suppressed (a
324
+ // placeholder/example match) and maps to "note", SARIF's own tier for
325
+ // exactly that: worth showing, not worth alarming over.
326
+ const SARIF_LEVEL = { high: "error", medium: "warning", low: "note" };
327
+
328
+ /**
329
+ * SARIF 2.1.0 output (--sarif): the format GitHub's code-scanning Security
330
+ * tab, and inline pull-request annotations, both consume. residoo already
331
+ * ships a GitHub Action and a pre-commit hook, so not emitting the one
332
+ * format that plugs a scan straight into GitHub's native UI was a real gap
333
+ * for exactly the CI audience those two things target.
334
+ *
335
+ * Scoped to secret findings only (result.findings), not the separate
336
+ * integrity checks (planted hooks, droppers): those don't share the same
337
+ * per-line, per-file location shape, and forcing them into one schema badly
338
+ * would be worse than a stated, honest scope limit. --json remains the
339
+ * format that carries everything (findings, integrity, rotation) together.
340
+ *
341
+ * partialFingerprints carries residoo's own stable fingerprint
342
+ * (fingerprintFinding, already proven stable across line-number and
343
+ * directory changes, see tests/smoke.js) under a versioned key, so GitHub's
344
+ * own alert-dedup logic can track one finding across reruns without
345
+ * depending on line numbers moving, exactly the property SARIF's
346
+ * fingerprinting is designed around.
347
+ */
348
+ function renderSarif(result) {
349
+ const { version } = require("../package.json");
350
+ const rules = new Map();
351
+ const results = result.findings.map((f) => {
352
+ if (!rules.has(f.ruleId)) {
353
+ rules.set(f.ruleId, {
354
+ id: f.ruleId,
355
+ name: f.label,
356
+ shortDescription: { text: f.label },
357
+ properties: { "security-severity": f.confidence === "high" ? "9.0" : f.confidence === "medium" ? "6.0" : "3.0" },
358
+ });
359
+ }
360
+ return {
361
+ ruleId: f.ruleId,
362
+ level: SARIF_LEVEL[f.confidence] || "warning",
363
+ message: { text: `${f.label} (redacted: ${f.preview})` },
364
+ locations: [{
365
+ physicalLocation: {
366
+ artifactLocation: { uri: f.relFile },
367
+ ...(Number.isInteger(f.line) ? { region: { startLine: f.line } } : {}),
368
+ },
369
+ }],
370
+ partialFingerprints: { "residooFingerprint/v1": fingerprintFinding(f) },
371
+ properties: { source: f.source, confidence: f.confidence },
372
+ };
373
+ });
374
+
375
+ return JSON.stringify({
376
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
377
+ version: "2.1.0",
378
+ runs: [{
379
+ tool: {
380
+ driver: {
381
+ name: "residoo",
382
+ version,
383
+ informationUri: "https://github.com/dandovdub/residoo",
384
+ rules: [...rules.values()],
385
+ },
386
+ },
387
+ results,
388
+ }],
389
+ }, null, 2);
390
+ }
391
+
392
+ module.exports = { render, renderIntegrity, renderRotationSection, renderJson, renderSarif };