@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 +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 +3 -2
package/dist/skill.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrites every repository-relative markdown link in `markdown` to a raw
|
|
3
|
+
* permalink pinned to `pinRef`. Absolute URLs, protocol-relative URLs, and
|
|
4
|
+
* bare fragments are left untouched.
|
|
5
|
+
*/
|
|
6
|
+
export declare function rewriteRelativeLinks(markdown: string, skillSourceDirectory: string, pinRef: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Extracts the `description:` scalar from a skill document's leading YAML
|
|
9
|
+
* frontmatter block. Returns undefined when the document has no frontmatter,
|
|
10
|
+
* declares no description, or uses a multi-line block scalar.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractFrontmatterDescription(document: string): string | undefined;
|
package/dist/skill.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
* Shared helper for packages that ship a SKILL.md: rewrites repository-relative
|
|
5
|
+
* markdown links to commit-pinned raw permalinks so the emitted skill resolves
|
|
6
|
+
* its references against the exact repository state that shipped the package.
|
|
7
|
+
*
|
|
8
|
+
* Ported from src/Sarif.Multitool.Library/GetSkill/GetSkillCommand.cs
|
|
9
|
+
* (RewriteRelativeLinks / ResolveRepositoryRelative / IsRepositoryRelative).
|
|
10
|
+
*/
|
|
11
|
+
const RAW_CONTENT_BASE_URL = 'https://raw.githubusercontent.com/microsoft/sarif-sdk/';
|
|
12
|
+
const MARKDOWN_LINK = /\]\(([^)\s]+)(\s+"[^"]*")?\)/g;
|
|
13
|
+
const URI_SCHEME = /^[a-zA-Z][a-zA-Z0-9+.\-]*:/;
|
|
14
|
+
/**
|
|
15
|
+
* Rewrites every repository-relative markdown link in `markdown` to a raw
|
|
16
|
+
* permalink pinned to `pinRef`. Absolute URLs, protocol-relative URLs, and
|
|
17
|
+
* bare fragments are left untouched.
|
|
18
|
+
*/
|
|
19
|
+
export function rewriteRelativeLinks(markdown, skillSourceDirectory, pinRef) {
|
|
20
|
+
return markdown.replace(MARKDOWN_LINK, (match, url, title) => {
|
|
21
|
+
if (!isRepositoryRelative(url))
|
|
22
|
+
return match;
|
|
23
|
+
const fragmentIdx = url.indexOf('#');
|
|
24
|
+
const path = fragmentIdx >= 0 ? url.slice(0, fragmentIdx) : url;
|
|
25
|
+
const fragment = fragmentIdx >= 0 ? url.slice(fragmentIdx) : '';
|
|
26
|
+
const repoPath = resolveRepositoryRelative(skillSourceDirectory, path);
|
|
27
|
+
return `](${RAW_CONTENT_BASE_URL}${pinRef}/${repoPath}${fragment}${title ?? ''})`;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function isRepositoryRelative(url) {
|
|
31
|
+
if (!url)
|
|
32
|
+
return false;
|
|
33
|
+
if (url[0] === '#')
|
|
34
|
+
return false;
|
|
35
|
+
if (url.startsWith('//'))
|
|
36
|
+
return false;
|
|
37
|
+
if (URI_SCHEME.test(url))
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
function resolveRepositoryRelative(baseDirectory, relativePath) {
|
|
42
|
+
const segments = baseDirectory.split('/').filter((s) => s.length > 0);
|
|
43
|
+
for (const segment of relativePath.split('/')) {
|
|
44
|
+
if (segment.length === 0 || segment === '.')
|
|
45
|
+
continue;
|
|
46
|
+
if (segment === '..') {
|
|
47
|
+
if (segments.length > 0)
|
|
48
|
+
segments.pop();
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
segments.push(segment);
|
|
52
|
+
}
|
|
53
|
+
return segments.join('/');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extracts the `description:` scalar from a skill document's leading YAML
|
|
57
|
+
* frontmatter block. Returns undefined when the document has no frontmatter,
|
|
58
|
+
* declares no description, or uses a multi-line block scalar.
|
|
59
|
+
*/
|
|
60
|
+
export function extractFrontmatterDescription(document) {
|
|
61
|
+
if (!document)
|
|
62
|
+
return undefined;
|
|
63
|
+
const lines = document.split(/\r?\n/);
|
|
64
|
+
if (lines[0]?.trim() !== '---')
|
|
65
|
+
return undefined;
|
|
66
|
+
for (let i = 1; i < lines.length; i++) {
|
|
67
|
+
if (lines[i].trim() === '---')
|
|
68
|
+
return undefined;
|
|
69
|
+
const m = /^description:[ \t]*(.+?)[ \t]*$/.exec(lines[i]);
|
|
70
|
+
if (!m)
|
|
71
|
+
continue;
|
|
72
|
+
let value = m[1];
|
|
73
|
+
if (value.length >= 2 && (value[0] === '"' || value[0] === "'") && value.at(-1) === value[0]) {
|
|
74
|
+
value = value.slice(1, -1);
|
|
75
|
+
}
|
|
76
|
+
if (/^[>|][0-9+\-]*$/.test(value))
|
|
77
|
+
return undefined;
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
package/dist/vcp.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for turning a `versionControlProvenance.repositoryUri`
|
|
3
|
+
* into a portable artifact root. emit-run validates the shape at receipt;
|
|
4
|
+
* emit-finalize mints the root.
|
|
5
|
+
*
|
|
6
|
+
* Ported from src/Sarif.Multitool.Library/Emit/VcpPortableRoot.cs.
|
|
7
|
+
*/
|
|
8
|
+
import type { Run } from './sarif.js';
|
|
9
|
+
export interface PortableRoot {
|
|
10
|
+
portableRoot: string;
|
|
11
|
+
canonicalRepositoryUri: string;
|
|
12
|
+
leaf: string;
|
|
13
|
+
revisionWebUrl: string;
|
|
14
|
+
}
|
|
15
|
+
type Result<T> = {
|
|
16
|
+
ok: true;
|
|
17
|
+
value: T;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
error: string;
|
|
21
|
+
};
|
|
22
|
+
/** Validates that a portable root can be derived (no revisionId required). */
|
|
23
|
+
export declare function tryValidateRepositoryUri(rawRepositoryUri: string): Result<{
|
|
24
|
+
leaf: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** Mints the portable root for a repository URI + revision id. */
|
|
27
|
+
export declare function tryDerivePortableRoot(rawRepositoryUri: string, revisionId: string): Result<PortableRoot>;
|
|
28
|
+
/**
|
|
29
|
+
* Default-deny: true only when the run carries ≥1 VCP entry and every entry's
|
|
30
|
+
* repositoryUri classifies as a supported GitHub host. Mirrors
|
|
31
|
+
* VcpPortableRoot.IsGitHubHostedRun.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isGitHubHostedRun(run: Run): boolean;
|
|
34
|
+
export {};
|
package/dist/vcp.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
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 AZURE_DEVOPS_HOST = 'dev.azure.com';
|
|
4
|
+
const AZURE_DEVOPS_SSH_HOST = 'ssh.dev.azure.com';
|
|
5
|
+
const INVALID_SCP_HOST_CHARS = /[@/\\:?#\s]/;
|
|
6
|
+
function ok(value) {
|
|
7
|
+
return { ok: true, value };
|
|
8
|
+
}
|
|
9
|
+
function err(error) {
|
|
10
|
+
return { ok: false, error };
|
|
11
|
+
}
|
|
12
|
+
function isGitHubHost(host) {
|
|
13
|
+
const h = host.toLowerCase();
|
|
14
|
+
return h === 'github.com' || h.endsWith('.ghe.com');
|
|
15
|
+
}
|
|
16
|
+
function isLegacyAzureDevOpsHost(host) {
|
|
17
|
+
const h = host.toLowerCase();
|
|
18
|
+
return h === 'visualstudio.com' || h.endsWith('.visualstudio.com');
|
|
19
|
+
}
|
|
20
|
+
function sanitizeForDisplay(u) {
|
|
21
|
+
// Never echo credentials or query/fragment in diagnostics.
|
|
22
|
+
return `${u.protocol}//${u.host}${u.pathname}`;
|
|
23
|
+
}
|
|
24
|
+
function isSingleSafeSegment(rawSegment) {
|
|
25
|
+
if (!rawSegment)
|
|
26
|
+
return false;
|
|
27
|
+
const decoded = decodeURIComponent(rawSegment);
|
|
28
|
+
return (decoded.length !== 0 &&
|
|
29
|
+
decoded !== '.' &&
|
|
30
|
+
decoded !== '..' &&
|
|
31
|
+
!decoded.includes('/') &&
|
|
32
|
+
!decoded.includes('\\'));
|
|
33
|
+
}
|
|
34
|
+
function normalizeRepoSegment(rawSegment) {
|
|
35
|
+
let repo = rawSegment;
|
|
36
|
+
if (repo.toLowerCase().endsWith('.git'))
|
|
37
|
+
repo = repo.slice(0, -4);
|
|
38
|
+
const decoded = decodeURIComponent(repo);
|
|
39
|
+
if (decoded.length === 0 ||
|
|
40
|
+
decoded === '.' ||
|
|
41
|
+
decoded === '..' ||
|
|
42
|
+
decoded.includes('/') ||
|
|
43
|
+
decoded.includes('\\')) {
|
|
44
|
+
return err('the repository name is empty or is not a single valid path segment.');
|
|
45
|
+
}
|
|
46
|
+
return ok({ repoForUrl: repo, leaf: decoded });
|
|
47
|
+
}
|
|
48
|
+
// scp syntax: [user@]host:path with no scheme and no slash before the colon.
|
|
49
|
+
function tryParseScp(raw) {
|
|
50
|
+
if (!raw || raw.includes('://'))
|
|
51
|
+
return undefined;
|
|
52
|
+
const colon = raw.indexOf(':');
|
|
53
|
+
if (colon < 0)
|
|
54
|
+
return undefined;
|
|
55
|
+
const authority = raw.slice(0, colon);
|
|
56
|
+
if (authority.length === 0 || authority.includes('/'))
|
|
57
|
+
return undefined;
|
|
58
|
+
let userInfo = '';
|
|
59
|
+
let host;
|
|
60
|
+
const at = authority.indexOf('@');
|
|
61
|
+
if (at >= 0) {
|
|
62
|
+
if (authority.indexOf('@', at + 1) >= 0)
|
|
63
|
+
return undefined; // ambiguous double @
|
|
64
|
+
userInfo = authority.slice(0, at);
|
|
65
|
+
host = authority.slice(at + 1);
|
|
66
|
+
if (userInfo.length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
host = authority;
|
|
71
|
+
}
|
|
72
|
+
if (host.length === 0 || INVALID_SCP_HOST_CHARS.test(host))
|
|
73
|
+
return undefined;
|
|
74
|
+
const path = raw.slice(colon + 1);
|
|
75
|
+
if (path.includes('@'))
|
|
76
|
+
return undefined; // colon was inside user:password
|
|
77
|
+
if (path.length === 0)
|
|
78
|
+
return undefined;
|
|
79
|
+
return { host, userInfo, path };
|
|
80
|
+
}
|
|
81
|
+
function tryNormalizeToHttps(rawRepositoryUri) {
|
|
82
|
+
// Try absolute URL first.
|
|
83
|
+
let absolute;
|
|
84
|
+
try {
|
|
85
|
+
absolute = new URL(rawRepositoryUri);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* maybe scp form */
|
|
89
|
+
}
|
|
90
|
+
if (absolute && absolute.protocol === 'https:')
|
|
91
|
+
return ok(absolute);
|
|
92
|
+
let host, userInfo, path;
|
|
93
|
+
if (absolute && absolute.protocol === 'ssh:') {
|
|
94
|
+
if (absolute.port) {
|
|
95
|
+
return err('an ssh repositoryUri must use the default port; supply the https clone URL instead.');
|
|
96
|
+
}
|
|
97
|
+
if (absolute.search || absolute.hash) {
|
|
98
|
+
return err('an ssh repositoryUri must be a bare clone URL with no query or fragment; supply the https clone URL instead.');
|
|
99
|
+
}
|
|
100
|
+
host = absolute.hostname;
|
|
101
|
+
userInfo = absolute.username + (absolute.password ? `:${absolute.password}` : '');
|
|
102
|
+
path = absolute.pathname;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const scp = tryParseScp(rawRepositoryUri);
|
|
106
|
+
if (!scp) {
|
|
107
|
+
return err('repositoryUri must be an absolute https URL, or a github-compatible ssh/scp clone URL (git@host:owner/repo).');
|
|
108
|
+
}
|
|
109
|
+
({ host, userInfo, path } = scp);
|
|
110
|
+
}
|
|
111
|
+
if (userInfo.includes(':')) {
|
|
112
|
+
return err('a repositoryUri carrying credentials (user:password@) cannot be used to derive a portable root.');
|
|
113
|
+
}
|
|
114
|
+
const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
115
|
+
if (host.toLowerCase() === AZURE_DEVOPS_SSH_HOST ||
|
|
116
|
+
normalizedPath.toLowerCase().startsWith('v3/')) {
|
|
117
|
+
return err('azure devops ssh repositoryUri normalization is not supported; supply the https clone URL (https://dev.azure.com/<org>/<project>/_git/<repo>).');
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return ok(new URL(`https://${host}/${normalizedPath}`));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return err('repositoryUri must be an absolute https URL, or a github-compatible ssh/scp clone URL (git@host:owner/repo).');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function classify(rawRepositoryUri) {
|
|
127
|
+
if (!rawRepositoryUri) {
|
|
128
|
+
return err('repositoryUri is required so a portable root can be derived.');
|
|
129
|
+
}
|
|
130
|
+
const norm = tryNormalizeToHttps(rawRepositoryUri);
|
|
131
|
+
if (!norm.ok)
|
|
132
|
+
return norm;
|
|
133
|
+
const u = norm.value;
|
|
134
|
+
const display = sanitizeForDisplay(u);
|
|
135
|
+
if (u.port) {
|
|
136
|
+
return err(`repositoryUri '${display}' must use the host's default port.`);
|
|
137
|
+
}
|
|
138
|
+
if (u.search || u.hash) {
|
|
139
|
+
return err(`repositoryUri '${display}' must be a bare repository URL with no query or fragment.`);
|
|
140
|
+
}
|
|
141
|
+
if (u.username || u.password) {
|
|
142
|
+
return err('a repositoryUri must not carry embedded credentials (no account@ or account:password@); supply the clean https repository URL.');
|
|
143
|
+
}
|
|
144
|
+
const segments = u.pathname.split('/').filter((s) => s.length > 0);
|
|
145
|
+
const schemeAndServer = `${u.protocol}//${u.host}`;
|
|
146
|
+
const host = u.hostname;
|
|
147
|
+
if (isGitHubHost(host)) {
|
|
148
|
+
if (segments.length !== 2) {
|
|
149
|
+
return err(`github repositoryUri must take the form https://<host>/<owner>/<repo>; '${display}' did not.`);
|
|
150
|
+
}
|
|
151
|
+
const repo = normalizeRepoSegment(segments[1]);
|
|
152
|
+
if (!repo.ok)
|
|
153
|
+
return err(`github repositoryUri '${display}': ${repo.error}`);
|
|
154
|
+
return ok({
|
|
155
|
+
isGitHub: true,
|
|
156
|
+
isAzureDevOps: false,
|
|
157
|
+
schemeAndServer,
|
|
158
|
+
owner: segments[0],
|
|
159
|
+
repoForUrl: repo.value.repoForUrl,
|
|
160
|
+
leaf: repo.value.leaf,
|
|
161
|
+
display,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (host.toLowerCase() === AZURE_DEVOPS_HOST) {
|
|
165
|
+
if (segments.length !== 4 || segments[2].toLowerCase() !== '_git') {
|
|
166
|
+
return err(`azure devops repositoryUri must take the form https://dev.azure.com/<org>/<project>/_git/<repo>; '${display}' did not.`);
|
|
167
|
+
}
|
|
168
|
+
if (!isSingleSafeSegment(segments[0]) || !isSingleSafeSegment(segments[1])) {
|
|
169
|
+
return err(`azure devops repositoryUri '${display}': the organization and project must each be a single valid path segment.`);
|
|
170
|
+
}
|
|
171
|
+
const repo = normalizeRepoSegment(segments[3]);
|
|
172
|
+
if (!repo.ok)
|
|
173
|
+
return err(`azure devops repositoryUri '${display}': ${repo.error}`);
|
|
174
|
+
return ok({
|
|
175
|
+
isGitHub: false,
|
|
176
|
+
isAzureDevOps: true,
|
|
177
|
+
schemeAndServer,
|
|
178
|
+
adoPrefix: `${segments[0]}/${segments[1]}`,
|
|
179
|
+
repoForUrl: repo.value.repoForUrl,
|
|
180
|
+
leaf: repo.value.leaf,
|
|
181
|
+
display,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (isLegacyAzureDevOpsHost(host)) {
|
|
185
|
+
return err(`the legacy Azure DevOps host form is not supported; supply the repository as https://dev.azure.com/<org>/<project>/_git/<repo>. '${display}' did not.`);
|
|
186
|
+
}
|
|
187
|
+
return err(`portable root derivation supports GitHub (github.com or <slug>.ghe.com) and Azure DevOps (dev.azure.com); '${display}' is not a supported host.`);
|
|
188
|
+
}
|
|
189
|
+
/** Validates that a portable root can be derived (no revisionId required). */
|
|
190
|
+
export function tryValidateRepositoryUri(rawRepositoryUri) {
|
|
191
|
+
const c = classify(rawRepositoryUri);
|
|
192
|
+
return c.ok ? ok({ leaf: c.value.leaf }) : c;
|
|
193
|
+
}
|
|
194
|
+
/** Mints the portable root for a repository URI + revision id. */
|
|
195
|
+
export function tryDerivePortableRoot(rawRepositoryUri, revisionId) {
|
|
196
|
+
const cr = classify(rawRepositoryUri);
|
|
197
|
+
if (!cr.ok)
|
|
198
|
+
return cr;
|
|
199
|
+
const c = cr.value;
|
|
200
|
+
const canonicalRepositoryUri = c.display;
|
|
201
|
+
const rev = encodeURIComponent(revisionId);
|
|
202
|
+
const portableRoot = c.isAzureDevOps
|
|
203
|
+
? `${c.schemeAndServer}/${c.adoPrefix}/_git/${c.repoForUrl}/`
|
|
204
|
+
: `${c.schemeAndServer}/${c.owner}/${c.repoForUrl}/blob/${rev}/`;
|
|
205
|
+
const revisionWebUrl = c.isAzureDevOps
|
|
206
|
+
? `${c.schemeAndServer}/${c.adoPrefix}/_git/${c.repoForUrl}?version=GC${rev}`
|
|
207
|
+
: `${c.schemeAndServer}/${c.owner}/${c.repoForUrl}/tree/${rev}`;
|
|
208
|
+
return ok({ portableRoot, canonicalRepositoryUri, leaf: c.leaf, revisionWebUrl });
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Default-deny: true only when the run carries ≥1 VCP entry and every entry's
|
|
212
|
+
* repositoryUri classifies as a supported GitHub host. Mirrors
|
|
213
|
+
* VcpPortableRoot.IsGitHubHostedRun.
|
|
214
|
+
*/
|
|
215
|
+
export function isGitHubHostedRun(run) {
|
|
216
|
+
const vcp = run.versionControlProvenance;
|
|
217
|
+
if (!vcp || vcp.length === 0)
|
|
218
|
+
return false;
|
|
219
|
+
for (const d of vcp) {
|
|
220
|
+
if (!d)
|
|
221
|
+
return false;
|
|
222
|
+
const c = classify(d.repositoryUri);
|
|
223
|
+
if (!c.ok || !c.value.isGitHub)
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/sarif",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "SARIF SDK for Node.js: open-typed object model, region/snippet resolution, AI ruleId convention, and serialization helpers. Native TypeScript; no CLR dependency.",
|
|
5
5
|
"author": "Microsoft",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "tsc"
|
|
33
|
+
"build": "tsc && node scripts/generate-serialization-model.mjs",
|
|
34
|
+
"test": "npm run build && node --test test/insertOptionalData.test.js test/regions.test.js test/rollingHash.test.js test/vcp.test.js"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@types/node": "^20.14.0",
|