@microsoft/sarif 0.0.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,83 @@
1
1
  # @microsoft/sarif
2
2
 
3
- SARIF SDK for Node.js — native-TypeScript open-typed object model, region/snippet
4
- resolution, AI ruleId convention, and serialization helpers. No CLR dependency.
3
+ SARIF SDK for Node.js — the base layer that
4
+ [`@microsoft/sarif-multitool-ts`](https://www.npmjs.com/package/@microsoft/sarif-multitool-ts)
5
+ and third-party SARIF producers build on. Native TypeScript; **no CLR
6
+ dependency.**
5
7
 
6
- > **v0.0.x is a placeholder** that reserves the package name and proves the
7
- > publish pipeline. The implementation lands in a subsequent release; track
8
- > progress at https://github.com/microsoft/sarif-sdk.
8
+ ## What's here
9
9
 
10
- This is the base package. For the multitool verbs (`emit-*`, `get-*`,
11
- `validate`) see [`@microsoft/sarif-multitool-ts`](https://www.npmjs.com/package/@microsoft/sarif-multitool-ts).
10
+ | | Ported from |
11
+ |---|---|
12
+ | **Open-typed SARIF 2.1.0 object model** — every node carries `properties?` + `[extra: string]: unknown` so caller-supplied data round-trips verbatim | the SARIF schema (subset) |
13
+ | `FileRegionsCache`, `NewLineIndex` — line/col ↔ charOffset, snippet + contextRegion extraction, sha-256 | `src/Sarif/FileRegionsCache.cs`, `NewLineIndex.cs` |
14
+ | `AIRuleIdConvention` — `CWE-<n>/<sub>` \| `NOVEL-<sub>` grammar | `src/Sarif/Emit/AIRuleIdConvention.cs` |
15
+ | `tryDerivePortableRoot`, `isGitHubHostedRun` — repo-URI → GitHub/ADO permalink root | `src/Sarif.Multitool.Library/Emit/VcpPortableRoot.cs` |
16
+ | `insertOptionalData`, `tryReconstructAbsoluteUri` — region/hash enrichment visitor | `src/Sarif/Visitors/InsertOptionalDataVisitor.cs` |
17
+ | `atomicWrite`, `stripNulls`, `serializeSarifLog` — Newtonsoft-parity I/O | `src/Sarif/Emit/AtomicSarifWriter.cs` |
18
+ | `rewriteRelativeLinks`, `extractFrontmatterDescription` — SKILL.md link pinning | `src/Sarif.Multitool.Library/GetSkill/GetSkillCommand.cs` |
19
+
20
+ This package is a **port**; the C# under `src/Sarif/` is normative. When they
21
+ disagree, this package has a bug.
22
+
23
+ ## Byte-identity with the .NET multitool
24
+
25
+ `serializeSarifLog` is contracted to produce output **byte-for-byte identical**
26
+ to the raw `dotnet Sarif.Multitool emit-finalize` output for the same input.
27
+ That single-oracle contract is what lets the two runtimes share one set of
28
+ fixtures and fail mutually when either side drifts.
29
+
30
+ Getting to byte-identity required matching four Newtonsoft traits that
31
+ `JSON.stringify` does not reproduce on its own. The machinery lives in
32
+ [`src/canonicalize.ts`](src/canonicalize.ts) and is driven by a build-time
33
+ generated model, [`scripts/generate-serialization-model.mjs`](scripts/generate-serialization-model.mjs)
34
+ → `dist/serialization-model.json`:
35
+
36
+ 1. **Key order.** Newtonsoft writes each typed object's members in **C# property
37
+ declaration order, overridden by `[DataMember(Order=N)]` when present** — not
38
+ schema order and not insertion order. The generator reads that order straight
39
+ from the normative `src/Sarif/Autogenerated/*.cs` `[DataMember]` attributes,
40
+ and pairs it with the schema's `$ref` graph so the canonicalizer knows each
41
+ property's child type. There is no hand-maintained order table; the
42
+ autogenerated C# is the only source of truth.
43
+
44
+ 2. **Property bags preserve insertion order.** A `properties` bag (and anything
45
+ nested inside it) keeps the order the producer inserted keys in — bags are
46
+ *not* reordered to the model. This is why `security-severity` precedes `tags`
47
+ in an enriched rule: `emit-finalize` inserts them in that order.
48
+
49
+ 3. **Compact property-bag values.** Newtonsoft stores bag values as
50
+ pre-serialized `JToken` text and writes them back verbatim, so a bag array
51
+ lands on one line (`"tags": ["security","external/cwe/cwe-89"]`) while the
52
+ bag's *keys* stay indented. Typed arrays everywhere else are multi-line. The
53
+ emitter (`emitCanonicalJson`) reproduces exactly this mixed shape.
54
+
55
+ 4. **Newlines and trailing byte.** The indented writer uses
56
+ `Environment.NewLine` (CRLF on Windows, LF on Linux) and emits **no trailing
57
+ newline**. `serializeSarifLog` mirrors `os.EOL` and omits the trailing
58
+ newline, so a fixture generated on the same host matches on both runtimes.
59
+
60
+ The contract is enforced live, not against a committed golden, by the
61
+ conformance harness in
62
+ [`npm/sarif-multitool-ts/test/conformance.test.js`](../sarif-multitool-ts/test/conformance.test.js):
63
+ it runs each scenario through both the TS library and the real .NET multitool
64
+ and asserts raw byte-identity (after masking volatile `endTimeUtc` / temp
65
+ paths). Note the committed `*.sarif` taxonomy samples under `src/Sarif/` are
66
+ **not** the byte target — those are re-serialized by their PowerShell
67
+ `*GenerateSample.ps1` scripts (`ConvertTo-Json` + `Add-Member` patches), so they
68
+ gate "PowerShell + multitool", not the raw multitool wire shape. This port
69
+ targets the **raw** .NET output.
70
+
71
+ ## Usage
72
+
73
+ ```ts
74
+ import { FileRegionsCache, AIRuleIdConvention, serializeSarifLog,
75
+ type SarifLog, type Result } from '@microsoft/sarif';
76
+ ```
77
+
78
+ For the multitool verbs (`emit-*`, `get-*`, `validate`) and the `sarif` CLI,
79
+ install [`@microsoft/sarif-multitool-ts`](https://www.npmjs.com/package/@microsoft/sarif-multitool-ts)
80
+ instead — it re-exports everything here.
12
81
 
13
82
  ## License
14
83
 
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Enforces the SARIF SDK AI-authoring convention for `result.ruleId`.
3
+ * Ported from src/Sarif/Emit/AIRuleIdConvention.cs.
4
+ *
5
+ * Accepted forms:
6
+ * - `CWE-<number>/<sub-id>` where sub-id is lowercase-alphanumeric kebab-case
7
+ * e.g. `CWE-89/kql-injection-from-config`
8
+ * - `NOVEL-<sub-id>` (flat, no slash) for findings with no CWE mapping
9
+ */
10
+ import type { Result } from './sarif.js';
11
+ export declare class AIRuleIdConventionError extends Error {
12
+ readonly offenders: readonly string[];
13
+ constructor(offenders: readonly string[]);
14
+ }
15
+ export declare const AIRuleIdConvention: {
16
+ /**
17
+ * Extracts the canonical `CWE-<number>` id (leading-zero-free) from a ruleId,
18
+ * whether bare (`CWE-89`) or sub-id form (`CWE-89/kql-injection`). Returns
19
+ * undefined for NOVEL- ids, null/empty, or anything that does not start
20
+ * with a CWE token.
21
+ */
22
+ readonly tryGetCweId: (ruleId: string | undefined) => string | undefined;
23
+ readonly isNovel: (ruleId: string | undefined) => boolean;
24
+ readonly isAcceptable: (ruleId: string | undefined) => boolean;
25
+ readonly throwIfUnacceptable: (ruleId: string | undefined) => void;
26
+ readonly throwIfAnyUnacceptable: (results: ReadonlyArray<Result> | undefined) => void;
27
+ };
@@ -0,0 +1,60 @@
1
+ // Copyright (c) Microsoft. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+ const TAXONOMY_SUB_ID = /^CWE-[0-9]+\/[a-z0-9]+(-[a-z0-9]+)*$/;
4
+ const NOVEL_PREFIX = /^NOVEL-[a-z0-9]+(-[a-z0-9]+)*$/;
5
+ const CWE_NUMBER = /^CWE-([0-9]+)(\/|$)/;
6
+ const NOVEL_MARKER = 'NOVEL-';
7
+ export class AIRuleIdConventionError extends Error {
8
+ offenders;
9
+ constructor(offenders) {
10
+ const list = offenders.map((o) => `'${o}'`).join(', ');
11
+ super(`result.ruleId must follow the AI ruleId convention: 'CWE-<number>/<lowercase-kebab-sub-id>' or 'NOVEL-<lowercase-kebab-sub-id>'. Offending value(s): ${list}.`);
12
+ this.name = 'AIRuleIdConventionError';
13
+ this.offenders = offenders;
14
+ }
15
+ }
16
+ export const AIRuleIdConvention = {
17
+ /**
18
+ * Extracts the canonical `CWE-<number>` id (leading-zero-free) from a ruleId,
19
+ * whether bare (`CWE-89`) or sub-id form (`CWE-89/kql-injection`). Returns
20
+ * undefined for NOVEL- ids, null/empty, or anything that does not start
21
+ * with a CWE token.
22
+ */
23
+ tryGetCweId(ruleId) {
24
+ if (!ruleId)
25
+ return undefined;
26
+ const m = CWE_NUMBER.exec(ruleId);
27
+ if (!m)
28
+ return undefined;
29
+ const n = Number.parseInt(m[1], 10);
30
+ if (!Number.isFinite(n))
31
+ return undefined;
32
+ return `CWE-${n}`;
33
+ },
34
+ isNovel(ruleId) {
35
+ return !!ruleId && ruleId.startsWith(NOVEL_MARKER);
36
+ },
37
+ isAcceptable(ruleId) {
38
+ if (!ruleId)
39
+ return false;
40
+ if (ruleId.startsWith(NOVEL_MARKER))
41
+ return NOVEL_PREFIX.test(ruleId);
42
+ return TAXONOMY_SUB_ID.test(ruleId);
43
+ },
44
+ throwIfUnacceptable(ruleId) {
45
+ if (!this.isAcceptable(ruleId)) {
46
+ throw new AIRuleIdConventionError([ruleId ?? '']);
47
+ }
48
+ },
49
+ throwIfAnyUnacceptable(results) {
50
+ if (!results || results.length === 0)
51
+ return;
52
+ const offenders = [];
53
+ for (const r of results) {
54
+ if (!this.isAcceptable(r?.ruleId))
55
+ offenders.push(r?.ruleId ?? '');
56
+ }
57
+ if (offenders.length > 0)
58
+ throw new AIRuleIdConventionError(offenders);
59
+ },
60
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Returns a deep copy of `log` with every typed object's keys reordered to the
3
+ * .NET model order. The input is not mutated.
4
+ */
5
+ export declare function canonicalizeSarifLog<T>(log: T): T;
6
+ /**
7
+ * Serializes a SARIF log to the .NET multitool's byte shape: model-ordered keys,
8
+ * two-space indent, and verbatim-compact property-bag values. `prettyPrint=false`
9
+ * yields fully compact output (every value on one line), matching the .NET
10
+ * minified writer. Uses LF newlines; the caller applies the platform newline.
11
+ */
12
+ export declare function emitCanonicalJson(log: unknown, prettyPrint: boolean): string;
@@ -0,0 +1,141 @@
1
+ // Copyright (c) Microsoft. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+ /**
4
+ * Canonical key ordering for byte-identical parity with the .NET multitool.
5
+ *
6
+ * The .NET multitool serializes through its typed object model, so its on-disk
7
+ * key order is the model's DataMember order — independent of the order keys
8
+ * arrived in. A JS object, by contrast, serializes in insertion order. To match
9
+ * .NET byte-for-byte we reorder every typed node to the model order.
10
+ *
11
+ * The order + type graph live in dist/serialization-model.json, generated at
12
+ * build time from the same authorities that define .NET's behavior:
13
+ * - key order: src/Sarif/Autogenerated/*.cs DataMember attributes
14
+ * - prop -> child-type graph: the published SARIF schema's $ref edges
15
+ * See scripts/generate-serialization-model.mjs.
16
+ *
17
+ * Property bags (`@propertyBag`) are passed through untouched: their key order
18
+ * is producer-determined and preserved by both runtimes.
19
+ */
20
+ import { readFileSync } from 'node:fs';
21
+ const PROPERTY_BAG = '@propertyBag';
22
+ let modelCache;
23
+ function model() {
24
+ if (modelCache === undefined) {
25
+ const url = new URL('./serialization-model.json', import.meta.url);
26
+ modelCache = JSON.parse(readFileSync(url, 'utf8'));
27
+ }
28
+ return modelCache;
29
+ }
30
+ function orderValue(value, def) {
31
+ if (Array.isArray(value)) {
32
+ return value.map((v) => orderValue(v, def));
33
+ }
34
+ if (value !== null && typeof value === 'object') {
35
+ if (!def || def === PROPERTY_BAG)
36
+ return value; // property bag / untyped: preserve insertion order
37
+ const entry = model()[def];
38
+ if (!entry)
39
+ return value;
40
+ return orderObject(value, entry);
41
+ }
42
+ return value;
43
+ }
44
+ function orderObject(value, entry) {
45
+ const out = {};
46
+ for (const prop of entry.order) {
47
+ if (Object.prototype.hasOwnProperty.call(value, prop)) {
48
+ out[prop] = orderValue(value[prop], entry.children[prop]);
49
+ }
50
+ }
51
+ // Any keys the model doesn't know about (defensive; the typed model normally
52
+ // covers everything) keep their relative insertion order at the tail.
53
+ for (const key of Object.keys(value)) {
54
+ if (!Object.prototype.hasOwnProperty.call(out, key)) {
55
+ out[key] = value[key];
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+ /**
61
+ * Returns a deep copy of `log` with every typed object's keys reordered to the
62
+ * .NET model order. The input is not mutated.
63
+ */
64
+ export function canonicalizeSarifLog(log) {
65
+ return orderValue(log, 'sarifLog');
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // Canonical JSON emitter
69
+ // ---------------------------------------------------------------------------
70
+ //
71
+ // Newtonsoft (the .NET multitool's writer) serializes a SARIF log indented, but
72
+ // emits property-bag *values* verbatim-compact: they are stored as
73
+ // pre-serialized JToken text, so an array like `tags` lands on a single line
74
+ // (`"tags": ["security","external/cwe/cwe-89"]`) while the bag's keys stay
75
+ // indented. Typed arrays everywhere else are multi-line as usual. JSON.stringify
76
+ // cannot produce this mixed shape, so we emit it ourselves. This reorders to the
77
+ // model order in the same pass, so callers need not pre-canonicalize.
78
+ const INDENT = ' ';
79
+ function pad(depth) {
80
+ return INDENT.repeat(depth);
81
+ }
82
+ /** Emits a property bag: indented keys, but each value rendered verbatim-compact. */
83
+ function emitPropertyBag(value, depth) {
84
+ const keys = Object.keys(value);
85
+ if (keys.length === 0)
86
+ return '{}';
87
+ const inner = pad(depth + 1);
88
+ const lines = keys.map((k) => `${inner}${JSON.stringify(k)}: ${JSON.stringify(value[k])}`);
89
+ return `{\n${lines.join(',\n')}\n${pad(depth)}}`;
90
+ }
91
+ function emitValue(value, def, depth) {
92
+ if (Array.isArray(value)) {
93
+ if (value.length === 0)
94
+ return '[]';
95
+ const inner = pad(depth + 1);
96
+ const lines = value.map((v) => `${inner}${emitValue(v, def, depth + 1)}`);
97
+ return `[\n${lines.join(',\n')}\n${pad(depth)}]`;
98
+ }
99
+ if (value !== null && typeof value === 'object') {
100
+ if (def === PROPERTY_BAG)
101
+ return emitPropertyBag(value, depth);
102
+ const obj = value;
103
+ const entry = def ? model()[def] : undefined;
104
+ const keys = orderedKeys(obj, entry);
105
+ if (keys.length === 0)
106
+ return '{}';
107
+ const inner = pad(depth + 1);
108
+ const lines = keys.map((k) => `${inner}${JSON.stringify(k)}: ${emitValue(obj[k], entry?.children[k], depth + 1)}`);
109
+ return `{\n${lines.join(',\n')}\n${pad(depth)}}`;
110
+ }
111
+ return JSON.stringify(value);
112
+ }
113
+ /** Keys present on `obj`, in model order; unknown keys (defensive) trail in insertion order. */
114
+ function orderedKeys(obj, entry) {
115
+ if (!entry)
116
+ return Object.keys(obj);
117
+ const seen = new Set();
118
+ const out = [];
119
+ for (const prop of entry.order) {
120
+ if (Object.prototype.hasOwnProperty.call(obj, prop)) {
121
+ out.push(prop);
122
+ seen.add(prop);
123
+ }
124
+ }
125
+ for (const key of Object.keys(obj)) {
126
+ if (!seen.has(key))
127
+ out.push(key);
128
+ }
129
+ return out;
130
+ }
131
+ /**
132
+ * Serializes a SARIF log to the .NET multitool's byte shape: model-ordered keys,
133
+ * two-space indent, and verbatim-compact property-bag values. `prettyPrint=false`
134
+ * yields fully compact output (every value on one line), matching the .NET
135
+ * minified writer. Uses LF newlines; the caller applies the platform newline.
136
+ */
137
+ export function emitCanonicalJson(log, prettyPrint) {
138
+ if (!prettyPrint)
139
+ return JSON.stringify(canonicalizeSarifLog(log));
140
+ return emitValue(log, 'sarifLog', 0);
141
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,16 @@
1
1
  /**
2
2
  * @microsoft/sarif — SARIF SDK for Node.js.
3
3
  *
4
- * v0.0.x PLACEHOLDER. This release reserves the package name and proves the
5
- * publish pipeline; the implementation (open-typed SARIF object model,
6
- * FileRegionsCache, AIRuleIdConvention, serialization helpers) lands in a
7
- * subsequent release tracked at https://github.com/microsoft/sarif-sdk.
4
+ * Open-typed SARIF 2.1.0 object model, region/snippet resolution, AI ruleId
5
+ * convention, portable-root derivation, and serialization helpers. Every TS
6
+ * module names the C# source-of-truth file it was ported from. The C# under
7
+ * src/Sarif/ is normative; when they disagree this package has a bug.
8
8
  */
9
- export declare const SARIF_SCHEMA_URI = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/csd01/schemas/sarif-schema-2.1.0.json";
9
+ export * from './sarif.js';
10
+ export { FileRegionsCache, NewLineIndex } from './regions.js';
11
+ export { computeRollingHashes } from './rollingHash.js';
12
+ export { AIRuleIdConvention, AIRuleIdConventionError } from './aiRuleId.js';
13
+ export { tryValidateRepositoryUri, tryDerivePortableRoot, isGitHubHostedRun, type PortableRoot, } from './vcp.js';
14
+ export { insertOptionalData, tryReconstructAbsoluteUri, PRIMARY_LOCATION_LINE_HASH, type InsertOptionalDataFlags, } from './insertOptionalData.js';
15
+ export { atomicWrite, stripNulls, serializeSarifLog } from './io.js';
16
+ export { rewriteRelativeLinks, extractFrontmatterDescription } from './skill.js';
package/dist/index.js CHANGED
@@ -3,9 +3,16 @@
3
3
  /**
4
4
  * @microsoft/sarif — SARIF SDK for Node.js.
5
5
  *
6
- * v0.0.x PLACEHOLDER. This release reserves the package name and proves the
7
- * publish pipeline; the implementation (open-typed SARIF object model,
8
- * FileRegionsCache, AIRuleIdConvention, serialization helpers) lands in a
9
- * subsequent release tracked at https://github.com/microsoft/sarif-sdk.
6
+ * Open-typed SARIF 2.1.0 object model, region/snippet resolution, AI ruleId
7
+ * convention, portable-root derivation, and serialization helpers. Every TS
8
+ * module names the C# source-of-truth file it was ported from. The C# under
9
+ * src/Sarif/ is normative; when they disagree this package has a bug.
10
10
  */
11
- export const SARIF_SCHEMA_URI = 'https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/csd01/schemas/sarif-schema-2.1.0.json';
11
+ export * from './sarif.js';
12
+ export { FileRegionsCache, NewLineIndex } from './regions.js';
13
+ export { computeRollingHashes } from './rollingHash.js';
14
+ export { AIRuleIdConvention, AIRuleIdConventionError } from './aiRuleId.js';
15
+ export { tryValidateRepositoryUri, tryDerivePortableRoot, isGitHubHostedRun, } from './vcp.js';
16
+ export { insertOptionalData, tryReconstructAbsoluteUri, PRIMARY_LOCATION_LINE_HASH, } from './insertOptionalData.js';
17
+ export { atomicWrite, stripNulls, serializeSarifLog } from './io.js';
18
+ export { rewriteRelativeLinks, extractFrontmatterDescription } from './skill.js';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Subset of InsertOptionalDataVisitor needed by emit-finalize: walks every
3
+ * physical location, populates region snippets / context regions / char
4
+ * spans, and builds `run.artifacts[]` with sha-256 hashes.
5
+ *
6
+ * Ported from src/Sarif/Visitors/InsertOptionalDataVisitor.cs (the
7
+ * Hashes | RegionSnippets | ContextRegionSnippets | ComprehensiveRegionProperties
8
+ * subset; OverwriteExistingData is intentionally NOT set — see
9
+ * EmitFinalizeCommand.cs for rationale).
10
+ */
11
+ import type { Run, ArtifactLocation } from './sarif.js';
12
+ export interface InsertOptionalDataFlags {
13
+ hashes: boolean;
14
+ regionSnippets: boolean;
15
+ contextRegionSnippets: boolean;
16
+ comprehensiveRegionProperties: boolean;
17
+ /**
18
+ * Stamp the GitHub rolling-hash `primaryLocationLineHash` partial
19
+ * fingerprint on each result's primary location. GitHub-only — the raw
20
+ * code-scanning upload API does not backfill it. Off by default; existing
21
+ * values are preserved (OverwriteExistingData is not honored here).
22
+ */
23
+ rollingHashFingerprints?: boolean;
24
+ }
25
+ /** Partial-fingerprint key GitHub reads for cross-scan result de-duplication. */
26
+ export declare const PRIMARY_LOCATION_LINE_HASH = "primaryLocationLineHash";
27
+ /**
28
+ * Resolves an artifactLocation's `uri` + `uriBaseId` against the run's
29
+ * `originalUriBaseIds` to an absolute URI string, or undefined if it cannot
30
+ * be made absolute. Mirrors ArtifactLocation.TryReconstructAbsoluteUri.
31
+ */
32
+ export declare function tryReconstructAbsoluteUri(loc: ArtifactLocation | undefined, originalUriBaseIds: Record<string, ArtifactLocation> | undefined): string | undefined;
33
+ /**
34
+ * Walks the run mutating physical-location regions in place and populating
35
+ * `run.artifacts[]`. Returns the same run (mutation, not a copy — every
36
+ * caller-supplied key on every node is preserved per the pass-through
37
+ * guarantee).
38
+ */
39
+ export declare function insertOptionalData(run: Run, flags: InsertOptionalDataFlags): Run;
@@ -0,0 +1,173 @@
1
+ // Copyright (c) Microsoft. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+ import { FileRegionsCache } from './regions.js';
4
+ /** Partial-fingerprint key GitHub reads for cross-scan result de-duplication. */
5
+ export const PRIMARY_LOCATION_LINE_HASH = 'primaryLocationLineHash';
6
+ /**
7
+ * Resolves an artifactLocation's `uri` + `uriBaseId` against the run's
8
+ * `originalUriBaseIds` to an absolute URI string, or undefined if it cannot
9
+ * be made absolute. Mirrors ArtifactLocation.TryReconstructAbsoluteUri.
10
+ */
11
+ export function tryReconstructAbsoluteUri(loc, originalUriBaseIds) {
12
+ if (!loc?.uri)
13
+ return undefined;
14
+ // Already absolute?
15
+ try {
16
+ const u = new URL(loc.uri);
17
+ if (u.protocol)
18
+ return u.toString();
19
+ }
20
+ catch {
21
+ /* relative */
22
+ }
23
+ // Walk the uriBaseId chain.
24
+ let baseId = loc.uriBaseId;
25
+ let composed = loc.uri;
26
+ const seen = new Set();
27
+ while (baseId && originalUriBaseIds && !seen.has(baseId)) {
28
+ seen.add(baseId);
29
+ const base = originalUriBaseIds[baseId];
30
+ if (!base?.uri)
31
+ return undefined;
32
+ composed = base.uri.replace(/\/?$/, '/') + composed;
33
+ try {
34
+ const u = new URL(composed);
35
+ if (u.protocol)
36
+ return u.toString();
37
+ }
38
+ catch {
39
+ /* still relative */
40
+ }
41
+ baseId = base.uriBaseId;
42
+ }
43
+ return undefined;
44
+ }
45
+ /**
46
+ * Walks the run mutating physical-location regions in place and populating
47
+ * `run.artifacts[]`. Returns the same run (mutation, not a copy — every
48
+ * caller-supplied key on every node is preserved per the pass-through
49
+ * guarantee).
50
+ */
51
+ export function insertOptionalData(run, flags) {
52
+ const cache = new FileRegionsCache();
53
+ const oub = run.originalUriBaseIds;
54
+ // Scrape file references first so artifacts[] is populated before hashing.
55
+ if (flags.hashes) {
56
+ scrapeFileReferences(run);
57
+ }
58
+ for (const result of run.results ?? []) {
59
+ visitResultLocations(result, (pl) => visitPhysicalLocation(pl, oub, cache, flags));
60
+ }
61
+ if (flags.hashes && run.artifacts) {
62
+ for (const artifact of run.artifacts) {
63
+ const abs = tryReconstructAbsoluteUri(artifact.location, oub);
64
+ if (!abs)
65
+ continue;
66
+ if (!artifact.hashes) {
67
+ const sha = cache.getSha256(abs);
68
+ if (sha)
69
+ artifact.hashes = { 'sha-256': sha };
70
+ }
71
+ }
72
+ }
73
+ if (flags.rollingHashFingerprints) {
74
+ for (const result of run.results ?? []) {
75
+ stampPrimaryLocationLineHash(result, oub, cache);
76
+ }
77
+ }
78
+ return run;
79
+ }
80
+ /**
81
+ * Stamps the rolling-hash `primaryLocationLineHash` on a result's primary
82
+ * location, anchored to its startLine (a result with no region line pertains
83
+ * to the whole file and is fingerprinted from line 1, matching
84
+ * github/codeql-action fingerprints.ts). Mirrors the
85
+ * RollingHashPartialFingerprints branch of InsertOptionalDataVisitor.cs.
86
+ */
87
+ function stampPrimaryLocationLineHash(result, oub, cache) {
88
+ if (result.partialFingerprints?.[PRIMARY_LOCATION_LINE_HASH] !== undefined)
89
+ return;
90
+ const physicalLocation = result.locations?.[0]?.physicalLocation;
91
+ if (!physicalLocation?.artifactLocation)
92
+ return;
93
+ let startLine = physicalLocation.region?.startLine ?? 0;
94
+ if (startLine <= 0)
95
+ startLine = 1;
96
+ const resolvedUri = tryReconstructAbsoluteUri(physicalLocation.artifactLocation, oub);
97
+ if (!resolvedUri)
98
+ return;
99
+ const lineHash = cache.getRollingHashes(resolvedUri)?.get(startLine);
100
+ if (lineHash === undefined)
101
+ return;
102
+ result.partialFingerprints ??= {};
103
+ result.partialFingerprints[PRIMARY_LOCATION_LINE_HASH] = lineHash;
104
+ }
105
+ function visitResultLocations(result, visit) {
106
+ const allLocations = [
107
+ ...(result.locations ?? []),
108
+ ...(result.relatedLocations ?? []),
109
+ ];
110
+ for (const loc of allLocations) {
111
+ if (loc?.physicalLocation)
112
+ visit(loc.physicalLocation);
113
+ }
114
+ }
115
+ function visitPhysicalLocation(node, oub, cache, flags) {
116
+ if (!node.region || node.region.byteOffset !== undefined)
117
+ return;
118
+ if (!flags.regionSnippets && !flags.contextRegionSnippets && !flags.comprehensiveRegionProperties) {
119
+ return;
120
+ }
121
+ const resolvedUri = tryReconstructAbsoluteUri(node.artifactLocation, oub);
122
+ if (!resolvedUri)
123
+ return;
124
+ const expanded = cache.populateTextRegionProperties(node.region, resolvedUri, flags.regionSnippets);
125
+ const originalSnippet = node.region.snippet;
126
+ if (flags.comprehensiveRegionProperties) {
127
+ // Replace the region wholesale (preserving any property bag the caller
128
+ // supplied — populateTextRegionProperties spreads the input).
129
+ node.region = expanded;
130
+ }
131
+ node.region.snippet = originalSnippet ?? expanded.snippet;
132
+ if (flags.contextRegionSnippets && !node.contextRegion) {
133
+ node.contextRegion = cache.constructMultilineContextSnippet(expanded, resolvedUri);
134
+ }
135
+ }
136
+ /**
137
+ * Builds run.artifacts[] from every artifactLocation referenced by results,
138
+ * deduplicating by (uri, uriBaseId). Mirrors AddFileReferencesVisitor.
139
+ */
140
+ function scrapeFileReferences(run) {
141
+ const artifacts = run.artifacts ? [...run.artifacts] : [];
142
+ const indexByKey = new Map();
143
+ for (let i = 0; i < artifacts.length; i++) {
144
+ const k = artifactKey(artifacts[i].location);
145
+ if (k)
146
+ indexByKey.set(k, i);
147
+ }
148
+ const register = (al) => {
149
+ if (!al?.uri)
150
+ return;
151
+ const k = artifactKey(al);
152
+ if (!k)
153
+ return;
154
+ let idx = indexByKey.get(k);
155
+ if (idx === undefined) {
156
+ idx = artifacts.length;
157
+ artifacts.push({ location: { uri: al.uri, uriBaseId: al.uriBaseId } });
158
+ indexByKey.set(k, idx);
159
+ }
160
+ // Back-reference into run.artifacts[] (mirrors AddFileReferencesVisitor).
161
+ al.index = idx;
162
+ };
163
+ for (const result of run.results ?? []) {
164
+ visitResultLocations(result, (pl) => register(pl.artifactLocation));
165
+ }
166
+ if (artifacts.length > 0)
167
+ run.artifacts = artifacts;
168
+ }
169
+ function artifactKey(al) {
170
+ if (!al?.uri)
171
+ return undefined;
172
+ return `${al.uriBaseId ?? ''}#${al.uri}`;
173
+ }
package/dist/io.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { SarifLog } from './sarif.js';
2
+ /**
3
+ * Atomically writes a file by staging to a sibling temp file in the same
4
+ * directory and renaming over the destination. Mirrors AtomicSarifWriter.Write.
5
+ */
6
+ export declare function atomicWrite(destinationPath: string, content: string): Promise<void>;
7
+ /**
8
+ * Recursively strips keys whose value is `null` or `undefined`, matching
9
+ * Newtonsoft `NullValueHandling.Ignore`. Arrays keep null entries (Newtonsoft
10
+ * does too); only object properties are pruned.
11
+ */
12
+ export declare function stripNulls<T>(value: T): T;
13
+ /**
14
+ * Serializes a SarifLog to match the .NET multitool byte-for-byte: model-order
15
+ * keys, two-space indent, null-stripping, verbatim-compact property-bag values
16
+ * (a Newtonsoft trait — bag values are stored as pre-serialized JToken text),
17
+ * `Environment.NewLine` line endings, and no trailing newline. See
18
+ * canonicalize.ts for the ordering/emit details.
19
+ */
20
+ export declare function serializeSarifLog(log: SarifLog, prettyPrint?: boolean): string;