@groma/scanner-go 0.1.1 → 0.1.2
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/dist/darwin-arm64/worker +0 -0
- package/dist/linux-arm64/worker +0 -0
- package/dist/linux-x64/worker +0 -0
- package/dist/win32-arm64/worker.exe +0 -0
- package/dist/win32-x64/worker.exe +0 -0
- package/package.json +1 -1
- package/src/index.js +175 -43
package/dist/darwin-arm64/worker
CHANGED
|
Binary file
|
package/dist/linux-arm64/worker
CHANGED
|
Binary file
|
package/dist/linux-x64/worker
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -24,9 +24,12 @@ var excluded = new Set([
|
|
|
24
24
|
"groma",
|
|
25
25
|
".groma"
|
|
26
26
|
]);
|
|
27
|
-
async function
|
|
27
|
+
async function repositoryFiles(root, matches) {
|
|
28
28
|
const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
|
|
29
|
-
return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) &&
|
|
29
|
+
return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && existsSync(path.join(root, file))))].sort();
|
|
30
|
+
}
|
|
31
|
+
function projectFiles(root, matches) {
|
|
32
|
+
return repositoryFiles(root, (file) => matches(file) && !file.split("/").some((part) => excluded.has(part)));
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
// plugins/scanners/project-scanner.ts
|
|
@@ -35,6 +38,116 @@ import path3 from "path";
|
|
|
35
38
|
// plugins/scanners/observations.ts
|
|
36
39
|
import path2 from "path";
|
|
37
40
|
|
|
41
|
+
// packages/scanner/src/values.ts
|
|
42
|
+
function object(value, label) {
|
|
43
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
44
|
+
throw new Error(`${label} must be an object`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function string(value, label) {
|
|
49
|
+
if (typeof value !== "string")
|
|
50
|
+
throw new Error(`${label} must be a string`);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function array(value, label) {
|
|
54
|
+
if (!Array.isArray(value))
|
|
55
|
+
throw new Error(`${label} must be an array`);
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// packages/scanner/src/http.ts
|
|
60
|
+
var pathText = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
|
|
61
|
+
var methodToken = /^[A-Z][A-Z-]*$/;
|
|
62
|
+
function fail(message) {
|
|
63
|
+
throw new Error(message);
|
|
64
|
+
}
|
|
65
|
+
function text(value, label) {
|
|
66
|
+
if (typeof value !== "string" || !pathText.test(value))
|
|
67
|
+
fail(`${label} must be nonempty URL path characters`);
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function method(value, label, anyMethod) {
|
|
71
|
+
if (typeof value !== "string" || !(methodToken.test(value) || anyMethod && value === "*")) {
|
|
72
|
+
fail(`${label} must be an uppercase HTTP method${anyMethod ? " or *" : ""}`);
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
function operation(value, operations) {
|
|
77
|
+
if (typeof value !== "string" || !operations.has(value))
|
|
78
|
+
fail("HTTP fact references unknown operation");
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function endpointSegment(value, last) {
|
|
82
|
+
const segment = object(value, "HTTP endpoint segment");
|
|
83
|
+
if (segment.kind === "literal")
|
|
84
|
+
return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
|
|
85
|
+
if (segment.kind !== "parameter" && segment.kind !== "catch-all")
|
|
86
|
+
fail(`unknown HTTP endpoint segment kind: ${String(segment.kind)}`);
|
|
87
|
+
if (segment.kind === "catch-all" && !last)
|
|
88
|
+
fail("an HTTP catch-all segment must be last");
|
|
89
|
+
for (const flag of ["optional", "constrained"]) {
|
|
90
|
+
if (segment[flag] !== undefined && typeof segment[flag] !== "boolean")
|
|
91
|
+
fail(`HTTP segment ${flag} must be a boolean`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
kind: segment.kind,
|
|
95
|
+
name: text(segment.name, "HTTP segment name"),
|
|
96
|
+
...segment.optional ? { optional: true } : {},
|
|
97
|
+
...segment.constrained ? { constrained: true } : {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function requestSegment(value) {
|
|
101
|
+
const segment = object(value, "HTTP request segment");
|
|
102
|
+
if (segment.kind === "literal")
|
|
103
|
+
return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
|
|
104
|
+
if (segment.kind === "dynamic" || segment.kind === "unknown")
|
|
105
|
+
return { kind: segment.kind };
|
|
106
|
+
return fail(`unknown HTTP request segment kind: ${String(segment.kind)}`);
|
|
107
|
+
}
|
|
108
|
+
function order(value) {
|
|
109
|
+
const fact = object(value, "HTTP endpoint order");
|
|
110
|
+
const application = string(fact.application, "HTTP endpoint order application");
|
|
111
|
+
if (application === "")
|
|
112
|
+
fail("HTTP endpoint order application must be nonempty");
|
|
113
|
+
const position = fact.position;
|
|
114
|
+
if (typeof position !== "number" || !Number.isInteger(position) || position < 0) {
|
|
115
|
+
fail("HTTP endpoint order position must be a nonnegative integer");
|
|
116
|
+
}
|
|
117
|
+
return { application, position };
|
|
118
|
+
}
|
|
119
|
+
function endpoint(value, operations) {
|
|
120
|
+
const fact = object(value, "HTTP endpoint");
|
|
121
|
+
const path = array(fact.path, "HTTP endpoint path");
|
|
122
|
+
return {
|
|
123
|
+
operation: operation(fact.operation, operations),
|
|
124
|
+
method: method(fact.method, "HTTP endpoint method", true),
|
|
125
|
+
path: path.map((segment, index) => endpointSegment(segment, index === path.length - 1)),
|
|
126
|
+
...fact.order === undefined ? {} : { order: order(fact.order) }
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function request(value, operations) {
|
|
130
|
+
const fact = object(value, "HTTP request");
|
|
131
|
+
if (fact.configured !== undefined && typeof fact.configured !== "boolean")
|
|
132
|
+
fail("HTTP request configured must be a boolean");
|
|
133
|
+
return {
|
|
134
|
+
operation: operation(fact.operation, operations),
|
|
135
|
+
...fact.method === undefined ? {} : { method: method(fact.method, "HTTP request method", false) },
|
|
136
|
+
...fact.configured ? { configured: true } : {},
|
|
137
|
+
path: array(fact.path, "HTTP request path").map(requestSegment)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function httpEvidence(endpoints, requests, operations) {
|
|
141
|
+
if (endpoints === undefined && requests === undefined)
|
|
142
|
+
return {};
|
|
143
|
+
if (operations === undefined)
|
|
144
|
+
fail("HTTP facts require operation declarations");
|
|
145
|
+
return {
|
|
146
|
+
...endpoints === undefined ? {} : { httpEndpoints: array(endpoints, "httpEndpoints").map((fact) => endpoint(fact, operations)) },
|
|
147
|
+
...requests === undefined ? {} : { httpRequests: array(requests, "httpRequests").map((fact) => request(fact, operations)) }
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
38
151
|
// packages/scanner/src/index.ts
|
|
39
152
|
function compare(...values) {
|
|
40
153
|
return values.join("\x00");
|
|
@@ -78,7 +191,9 @@ function createScanObservation(input) {
|
|
|
78
191
|
scanner: input.scanner,
|
|
79
192
|
roots,
|
|
80
193
|
files,
|
|
194
|
+
...sourceUnits(input.sourceUnits, filePaths),
|
|
81
195
|
...operationEvidence(input, filePaths),
|
|
196
|
+
...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
|
|
82
197
|
diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
|
|
83
198
|
diagnosticKey(diagnostic),
|
|
84
199
|
diagnostic
|
|
@@ -87,6 +202,23 @@ function createScanObservation(input) {
|
|
|
87
202
|
})
|
|
88
203
|
};
|
|
89
204
|
}
|
|
205
|
+
function sourceUnits(input, paths) {
|
|
206
|
+
if (input === undefined)
|
|
207
|
+
return {};
|
|
208
|
+
const units = array(input, "sourceUnits").map((entry) => {
|
|
209
|
+
const unit = object(entry, "source unit");
|
|
210
|
+
const primary = string(unit.primary, "source unit.primary");
|
|
211
|
+
const files = [...new Set(array(unit.files, "source unit.files").map((file) => string(file, "source unit.file")))].sort();
|
|
212
|
+
if (!files.includes(primary))
|
|
213
|
+
throw new Error(`source unit omits primary file: ${primary}`);
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
if (!paths.has(file))
|
|
216
|
+
throw new Error(`source unit references unknown file: ${file}`);
|
|
217
|
+
}
|
|
218
|
+
return { primary, files };
|
|
219
|
+
});
|
|
220
|
+
return { sourceUnits: [...new Map(units.map((unit) => [compare(unit.primary, ...unit.files), unit])).values()].sort((a, b) => compare(a.primary, ...a.files).localeCompare(compare(b.primary, ...b.files))) };
|
|
221
|
+
}
|
|
90
222
|
function validateRoots(input) {
|
|
91
223
|
const roots = uniqueBy(input, (root) => root.id, "root id");
|
|
92
224
|
const byId = new Map(roots.map((root) => [root.id, root]));
|
|
@@ -117,22 +249,6 @@ function diagnosticLocation(value) {
|
|
|
117
249
|
...value.line === undefined ? {} : { line: Number(value.line) }
|
|
118
250
|
};
|
|
119
251
|
}
|
|
120
|
-
function object(value, label) {
|
|
121
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
122
|
-
throw new Error(`${label} must be an object`);
|
|
123
|
-
}
|
|
124
|
-
return value;
|
|
125
|
-
}
|
|
126
|
-
function string(value, label) {
|
|
127
|
-
if (typeof value !== "string")
|
|
128
|
-
throw new Error(`${label} must be a string`);
|
|
129
|
-
return value;
|
|
130
|
-
}
|
|
131
|
-
function array(value, label) {
|
|
132
|
-
if (!Array.isArray(value))
|
|
133
|
-
throw new Error(`${label} must be an array`);
|
|
134
|
-
return value;
|
|
135
|
-
}
|
|
136
252
|
function parseScanObservation(source) {
|
|
137
253
|
const value = object(JSON.parse(source), "observation");
|
|
138
254
|
if (value.schemaVersion !== 1)
|
|
@@ -146,6 +262,9 @@ function parseScanObservation(source) {
|
|
|
146
262
|
engineVersion: string(scanner.engineVersion, "scanner.engineVersion")
|
|
147
263
|
},
|
|
148
264
|
...parseOperations(value),
|
|
265
|
+
httpEndpoints: value.httpEndpoints,
|
|
266
|
+
httpRequests: value.httpRequests,
|
|
267
|
+
...sourceUnits(value.sourceUnits, new Set(array(value.files, "files").map((entry) => string(object(entry, "file").file, "file.file")))),
|
|
149
268
|
roots: array(value.roots, "roots").map((entry) => {
|
|
150
269
|
const root = object(entry, "root");
|
|
151
270
|
return {
|
|
@@ -296,6 +415,9 @@ function relocateObservation(observation, directory) {
|
|
|
296
415
|
...observation,
|
|
297
416
|
roots: observation.roots.map((root) => ({ ...root, ...root.file ? { file: file(root.file) } : {} })),
|
|
298
417
|
files: observation.files.map((source) => ({ ...source, file: file(source.file) })),
|
|
418
|
+
...observation.sourceUnits === undefined ? {} : {
|
|
419
|
+
sourceUnits: observation.sourceUnits.map((unit) => ({ primary: file(unit.primary), files: unit.files.map(file) }))
|
|
420
|
+
},
|
|
299
421
|
operations: observation.operations?.map((operation) => ({ ...operation, file: file(operation.file) })),
|
|
300
422
|
invocations: observation.invocations?.map((call) => ({
|
|
301
423
|
...call,
|
|
@@ -312,6 +434,9 @@ function combineObservations(parts) {
|
|
|
312
434
|
const operations = [];
|
|
313
435
|
const invocations = [];
|
|
314
436
|
const diagnostics = [];
|
|
437
|
+
const sourceUnits = [];
|
|
438
|
+
const httpEndpoints = [];
|
|
439
|
+
const httpRequests = [];
|
|
315
440
|
for (const { key, observation } of parts) {
|
|
316
441
|
const id = (value) => JSON.stringify([key, value]);
|
|
317
442
|
roots.push(...observation.roots.map((root) => ({ ...root, id: id(root.id), ...root.parent ? { parent: id(root.parent) } : {} })));
|
|
@@ -323,7 +448,10 @@ function combineObservations(parts) {
|
|
|
323
448
|
}
|
|
324
449
|
operations.push(...(observation.operations ?? []).map((operation) => ({ ...operation, id: id(operation.id) })));
|
|
325
450
|
invocations.push(...(observation.invocations ?? []).map((call) => ({ ...call, source: id(call.source), targets: call.targets.map(id) })));
|
|
451
|
+
httpEndpoints.push(...(observation.httpEndpoints ?? []).map((endpoint) => ({ ...endpoint, operation: id(endpoint.operation) })));
|
|
452
|
+
httpRequests.push(...(observation.httpRequests ?? []).map((request) => ({ ...request, operation: id(request.operation) })));
|
|
326
453
|
diagnostics.push(...observation.diagnostics);
|
|
454
|
+
sourceUnits.push(...observation.sourceUnits ?? []);
|
|
327
455
|
}
|
|
328
456
|
return createScanObservation({
|
|
329
457
|
scanner: parts[0].observation.scanner,
|
|
@@ -331,7 +459,10 @@ function combineObservations(parts) {
|
|
|
331
459
|
files: [...files.values()],
|
|
332
460
|
operations,
|
|
333
461
|
invocations,
|
|
334
|
-
diagnostics
|
|
462
|
+
diagnostics,
|
|
463
|
+
...parts.some((part) => part.observation.sourceUnits !== undefined) ? { sourceUnits } : {},
|
|
464
|
+
...parts.some((part) => part.observation.httpEndpoints !== undefined) ? { httpEndpoints } : {},
|
|
465
|
+
...parts.some((part) => part.observation.httpRequests !== undefined) ? { httpRequests } : {}
|
|
335
466
|
});
|
|
336
467
|
}
|
|
337
468
|
|
|
@@ -365,9 +496,9 @@ import { access, realpath } from "fs/promises";
|
|
|
365
496
|
import path4 from "path";
|
|
366
497
|
import { fileURLToPath } from "url";
|
|
367
498
|
var packagedWorker = fileURLToPath(new URL(`../dist/${process.platform}-${process.arch}/worker${process.platform === "win32" ? ".exe" : ""}`, import.meta.url));
|
|
368
|
-
function run(command, args, root, env = process.env) {
|
|
499
|
+
function run(command, args, root, { env = process.env, input } = {}) {
|
|
369
500
|
return new Promise((resolve, reject) => {
|
|
370
|
-
execFile2(command, args, {
|
|
501
|
+
const child = execFile2(command, args, {
|
|
371
502
|
cwd: root,
|
|
372
503
|
env,
|
|
373
504
|
encoding: "utf8",
|
|
@@ -380,50 +511,51 @@ function run(command, args, root, env = process.env) {
|
|
|
380
511
|
else
|
|
381
512
|
resolve(stdout);
|
|
382
513
|
});
|
|
514
|
+
if (input !== undefined)
|
|
515
|
+
child.stdin?.end(input);
|
|
383
516
|
});
|
|
384
517
|
}
|
|
385
|
-
async function
|
|
386
|
-
const root = await realpath(repositoryRoot);
|
|
518
|
+
async function workerPath(options) {
|
|
387
519
|
const worker = options.worker ?? packagedWorker;
|
|
388
520
|
try {
|
|
389
521
|
await access(worker);
|
|
390
522
|
} catch {
|
|
391
523
|
throw new Error("GO_WORKER_MISSING: Install the packaged Go scanner for this platform, or build it with bun plugins/scanners/go/build.ts.");
|
|
392
524
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
await run(options.go ?? "go", ["list", "-mod=readonly", "-deps", "./..."], root, env);
|
|
406
|
-
} catch (error) {
|
|
407
|
-
throw new Error(`GO_PROJECT_PREPARATION: Prepare dependencies and generated source with the project's documented build commands (go mod download for module dependencies), then retry. Scan does not download tools or modules. ${error}`);
|
|
408
|
-
}
|
|
409
|
-
return { root, worker, env };
|
|
525
|
+
return worker;
|
|
526
|
+
}
|
|
527
|
+
async function checkGoReadiness(repositoryRoot, options = {}) {
|
|
528
|
+
const root = await realpath(repositoryRoot);
|
|
529
|
+
const worker = await workerPath(options);
|
|
530
|
+
await access(path4.join(root, "go.mod"));
|
|
531
|
+
return { root, worker };
|
|
532
|
+
}
|
|
533
|
+
async function readGoCodeStructure(repositoryRoot, references, options = {}) {
|
|
534
|
+
const worker = await workerPath(options);
|
|
535
|
+
return JSON.parse(await run(worker, ["outline", repositoryRoot], repositoryRoot, { input: JSON.stringify(references) }));
|
|
410
536
|
}
|
|
411
537
|
async function scanGoSource(repositoryRoot, options = {}) {
|
|
412
|
-
const { root, worker
|
|
538
|
+
const { root, worker } = await checkGoReadiness(repositoryRoot, options);
|
|
413
539
|
try {
|
|
414
|
-
return parseScanObservation(await run(worker, [root], root
|
|
540
|
+
return parseScanObservation(await run(worker, [root], root));
|
|
415
541
|
} catch (error) {
|
|
416
|
-
throw new Error(`
|
|
542
|
+
throw new Error(`GO_SOURCE_INVALID: No observation was produced. Check Go source syntax and module declarations. ${error}`);
|
|
417
543
|
}
|
|
418
544
|
}
|
|
419
545
|
|
|
420
546
|
// plugins/scanners/go/src/index.ts
|
|
547
|
+
async function goSources(root) {
|
|
548
|
+
const modules = (await projectFiles(root, (file) => path5.posix.basename(file) === "go.mod")).map((file) => path5.posix.dirname(file));
|
|
549
|
+
return projectFiles(root, (file) => file.endsWith(".go") && !file.endsWith("_test.go") && !file.split("/").some((part) => part === "testdata" || part.startsWith(".")) && modules.some((module) => module === "." || file.startsWith(`${module}/`)));
|
|
550
|
+
}
|
|
421
551
|
var scanner = {
|
|
422
552
|
id: "go",
|
|
423
553
|
watch: { include: ["**/*.go", "**/go.mod", "**/go.sum", "**/go.work"], exclude: [] },
|
|
554
|
+
listSourceFiles: goSources,
|
|
424
555
|
checkReadiness: async (root) => {
|
|
425
556
|
await checkGoReadiness(root);
|
|
426
557
|
},
|
|
558
|
+
readCodeStructure: readGoCodeStructure,
|
|
427
559
|
scan: scanGoSource
|
|
428
560
|
};
|
|
429
561
|
var src_default = projectScanner(scanner, async (root) => (await projectFiles(root, (file) => path5.posix.basename(file) === "go.mod")).map((file) => path5.dirname(path5.join(root, file))));
|