@microsoft/sarif 0.0.1 → 5.3.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 +76 -7
- package/dist/aiRuleId.d.ts +27 -0
- package/dist/aiRuleId.js +60 -0
- package/dist/canonicalize.d.ts +12 -0
- package/dist/canonicalize.js +141 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +12 -5
- package/dist/insertOptionalData.d.ts +39 -0
- package/dist/insertOptionalData.js +173 -0
- package/dist/io.d.ts +20 -0
- package/dist/io.js +77 -0
- package/dist/regions.d.ts +51 -0
- package/dist/regions.js +281 -0
- package/dist/rollingHash.d.ts +7 -0
- package/dist/rollingHash.js +111 -0
- package/dist/sarif.d.ts +227 -0
- package/dist/sarif.js +3 -0
- package/dist/serialization-model.json +949 -0
- package/dist/skill.d.ts +12 -0
- package/dist/skill.js +81 -0
- package/dist/vcp.d.ts +34 -0
- package/dist/vcp.js +227 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,14 +1,83 @@
|
|
|
1
1
|
# @microsoft/sarif
|
|
2
2
|
|
|
3
|
-
SARIF SDK for Node.js —
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
+
};
|
package/dist/aiRuleId.js
ADDED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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;
|