@dailephd/my-dev-kit-lab 0.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 +272 -0
- package/benchmarks/contracts/benchmark-project-profiles.json +1199 -0
- package/benchmarks/contracts/todo-behavior.md +70 -0
- package/benchmarks/contracts/todo-benchmark-case.json +227 -0
- package/benchmarks/projects/README.md +34 -0
- package/benchmarks/projects/task-analytics-large-mixed/README.md +1 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/__init__.py +3 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/fixtures.py +6 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/metrics.py +29 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/models.py +21 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/parser.py +16 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/pipeline.py +9 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/quality.py +8 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/reporting.py +11 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_metrics.py +19 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_parser.py +15 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_quality.py +19 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_reporting.py +15 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/package.json +12 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/index.ts +11 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/analyticsSnapshot.ts +20 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/project.ts +5 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/task.ts +10 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/reporting/buildProjectLeaderboard.ts +7 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/reporting/formatTaskHealthReport.ts +13 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/buildAnalyticsSnapshot.ts +39 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/completeTask.ts +10 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/createTask.ts +21 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/listTasksByProject.ts +6 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/store/projectStore.ts +20 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/store/taskStore.ts +44 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/validation/projectValidation.ts +12 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/validation/taskValidation.ts +18 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/buildAnalyticsSnapshot.test.ts +48 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/completeTask.test.ts +21 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/createTask.test.ts +31 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/listTasksByProject.test.ts +18 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/reporting.test.ts +19 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tsconfig.json +12 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/vitest.config.ts +5 -0
- package/benchmarks/projects/task-workflow-medium-ts/README.md +1 -0
- package/benchmarks/projects/task-workflow-medium-ts/package.json +12 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/index.ts +9 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/models/project.ts +6 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/models/task.ts +39 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/completeTask.ts +15 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/createTask.ts +26 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/filterTasks.ts +17 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/importTasks.ts +33 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/summarizeTasks.ts +30 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/store/taskStore.ts +76 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/utils/deterministicId.ts +3 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/validation/taskValidation.ts +45 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/completeTask.test.ts +16 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/createTask.test.ts +21 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/filterTasks.test.ts +18 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/importTasks.test.ts +22 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/summarizeTasks.test.ts +29 -0
- package/benchmarks/projects/task-workflow-medium-ts/tsconfig.json +12 -0
- package/benchmarks/projects/task-workflow-medium-ts/vitest.config.ts +5 -0
- package/benchmarks/projects/todo-js/README.md +3 -0
- package/benchmarks/projects/todo-js/package.json +11 -0
- package/benchmarks/projects/todo-js/src/index.js +2 -0
- package/benchmarks/projects/todo-js/src/taskService.js +37 -0
- package/benchmarks/projects/todo-js/src/taskStore.js +28 -0
- package/benchmarks/projects/todo-js/tests/taskService.test.js +45 -0
- package/benchmarks/projects/todo-js/vitest.config.js +5 -0
- package/benchmarks/projects/todo-mixed-ts-py/README.md +3 -0
- package/benchmarks/projects/todo-mixed-ts-py/package.json +13 -0
- package/benchmarks/projects/todo-mixed-ts-py/python/task_service.py +76 -0
- package/benchmarks/projects/todo-mixed-ts-py/src/taskCli.ts +38 -0
- package/benchmarks/projects/todo-mixed-ts-py/tests/mixedBoundary.test.ts +18 -0
- package/benchmarks/projects/todo-mixed-ts-py/tsconfig.json +12 -0
- package/benchmarks/projects/todo-mixed-ts-py/vitest.config.ts +5 -0
- package/benchmarks/projects/todo-python/README.md +3 -0
- package/benchmarks/projects/todo-python/src/__init__.py +4 -0
- package/benchmarks/projects/todo-python/src/task_service.py +32 -0
- package/benchmarks/projects/todo-python/src/task_store.py +28 -0
- package/benchmarks/projects/todo-python/tests/test_task_service.py +52 -0
- package/benchmarks/projects/todo-ts/README.md +3 -0
- package/benchmarks/projects/todo-ts/package.json +12 -0
- package/benchmarks/projects/todo-ts/src/index.ts +2 -0
- package/benchmarks/projects/todo-ts/src/taskService.ts +41 -0
- package/benchmarks/projects/todo-ts/src/taskStore.ts +34 -0
- package/benchmarks/projects/todo-ts/tests/taskService.test.ts +45 -0
- package/benchmarks/projects/todo-ts/tsconfig.json +12 -0
- package/benchmarks/projects/todo-ts/vitest.config.ts +5 -0
- package/dist/scripts/build-gallery.js +3 -0
- package/dist/scripts/capture-demo-report.js +3 -0
- package/dist/scripts/evaluate-token-savings.js +2 -0
- package/dist/scripts/experiments/describeExperiment.js +143 -0
- package/dist/scripts/experiments/listExperiments.js +44 -0
- package/dist/scripts/experiments/runExperiment.js +199 -0
- package/dist/scripts/generate-experiment-plots.js +3 -0
- package/dist/scripts/generate-prompt-variants.js +2 -0
- package/dist/scripts/render-experiment-report.js +2 -0
- package/dist/scripts/run-agent-prompt.js +2 -0
- package/dist/scripts/run-controlled-experiment.js +2 -0
- package/dist/scripts/run-final-demo.js +3 -0
- package/dist/scripts/run-lab-demo.js +5 -0
- package/dist/scripts/run-visualization-demos.js +3 -0
- package/dist/scripts/security/runCodeql.js +57 -0
- package/dist/scripts/security/runDependencyChecks.js +57 -0
- package/dist/scripts/security/runFuzzSmoke.js +29 -0
- package/dist/scripts/security/runPackageChecks.js +56 -0
- package/dist/scripts/security/runSemgrep.js +63 -0
- package/dist/scripts/security/validate.js +117 -0
- package/dist/scripts/verify-benchmarks.js +202 -0
- package/dist/src/agents/adapters/claudeAdapter.js +37 -0
- package/dist/src/agents/adapters/codexAdapter.js +110 -0
- package/dist/src/agents/adapters/fakeAgentAdapter.js +101 -0
- package/dist/src/agents/agentRegistry.js +21 -0
- package/dist/src/agents/index.js +7 -0
- package/dist/src/agents/parseAgentTokenUsage.js +137 -0
- package/dist/src/agents/runAgentPrompt.js +38 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/commands/buildGalleryCommand.js +56 -0
- package/dist/src/commands/captureDemoReport.js +116 -0
- package/dist/src/commands/evaluateTokenSavings.js +175 -0
- package/dist/src/commands/generateExperimentPlotsCommand.js +38 -0
- package/dist/src/commands/generatePromptVariants.js +67 -0
- package/dist/src/commands/renderExperimentReportCommand.js +131 -0
- package/dist/src/commands/runAgentPromptCommand.js +132 -0
- package/dist/src/commands/runControlledExperimentCommand.js +174 -0
- package/dist/src/commands/runFinalDemoCommand.js +123 -0
- package/dist/src/commands/runLabDemo.js +62 -0
- package/dist/src/commands/runVisualizationDemosCommand.js +67 -0
- package/dist/src/core/commandLine.js +59 -0
- package/dist/src/core/countTokens.js +8 -0
- package/dist/src/core/fileGlobs.js +100 -0
- package/dist/src/core/localProjectTarget.js +75 -0
- package/dist/src/core/pathSafety.js +19 -0
- package/dist/src/core/pythonCommand.js +30 -0
- package/dist/src/core/resolveCommand.js +110 -0
- package/dist/src/core/runMeasuredCommand.js +143 -0
- package/dist/src/evaluation/benchmarkMetadata.js +207 -0
- package/dist/src/evaluation/buildExperimentMatrix.js +75 -0
- package/dist/src/evaluation/classifyAgentRunOutcome.js +40 -0
- package/dist/src/evaluation/compareExperimentRuns.js +79 -0
- package/dist/src/evaluation/compareTokenSavings.js +47 -0
- package/dist/src/evaluation/controlledExperimentTypes.js +1 -0
- package/dist/src/evaluation/index.js +18 -0
- package/dist/src/evaluation/parseAgentAnswer.js +230 -0
- package/dist/src/evaluation/projectComplexity.js +126 -0
- package/dist/src/evaluation/projectFileTree.js +83 -0
- package/dist/src/evaluation/readEvaluationCases.js +59 -0
- package/dist/src/evaluation/renderTokenSavingsReportInput.js +55 -0
- package/dist/src/evaluation/runControlledExperiment.js +158 -0
- package/dist/src/evaluation/runMyDevKitRetrieval.js +197 -0
- package/dist/src/evaluation/runRawFullFileBaseline.js +31 -0
- package/dist/src/evaluation/scoreCorrectness.js +127 -0
- package/dist/src/evaluation/types.js +1 -0
- package/dist/src/evaluation/writeExperimentArtifacts.js +104 -0
- package/dist/src/evaluation/writeTokenSavingsArtifacts.js +57 -0
- package/dist/src/experiments/config.js +24 -0
- package/dist/src/experiments/defaultRegistry.js +7 -0
- package/dist/src/experiments/errors.js +18 -0
- package/dist/src/experiments/index.js +9 -0
- package/dist/src/experiments/outputPaths.js +25 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/config.js +37 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/index.js +3 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/plugin.js +83 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/resultMapping.js +260 -0
- package/dist/src/experiments/plugins/index.js +1 -0
- package/dist/src/experiments/registry.js +43 -0
- package/dist/src/experiments/results.js +48 -0
- package/dist/src/experiments/runner.js +181 -0
- package/dist/src/experiments/target.js +8 -0
- package/dist/src/experiments/types.js +1 -0
- package/dist/src/gallery/index.js +2 -0
- package/dist/src/gallery/types.js +1 -0
- package/dist/src/gallery/writeGalleryManifest.js +214 -0
- package/dist/src/index.js +12 -0
- package/dist/src/plots/buildExperimentPlotData.js +137 -0
- package/dist/src/plots/index.js +4 -0
- package/dist/src/plots/renderSvgChart.js +82 -0
- package/dist/src/plots/types.js +1 -0
- package/dist/src/plots/writePlotArtifacts.js +46 -0
- package/dist/src/prompts/buildPromptContext.js +68 -0
- package/dist/src/prompts/generateMyDevKitPrompt.js +106 -0
- package/dist/src/prompts/generatePromptVariants.js +36 -0
- package/dist/src/prompts/generateRawFullFilePrompt.js +97 -0
- package/dist/src/prompts/index.js +7 -0
- package/dist/src/prompts/measurePromptComplexity.js +41 -0
- package/dist/src/prompts/types.js +1 -0
- package/dist/src/prompts/writePromptArtifacts.js +43 -0
- package/dist/src/report/buildExperimentReportInput.js +339 -0
- package/dist/src/report/experimentReportTypes.js +1 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +153 -0
- package/dist/src/report/experiments/experimentReportModel.js +1 -0
- package/dist/src/report/experiments/index.js +4 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +133 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +30 -0
- package/dist/src/report/index.js +8 -0
- package/dist/src/report/renderExperimentHtmlReport.js +354 -0
- package/dist/src/report/renderHtmlReport.js +103 -0
- package/dist/src/report/types.js +10 -0
- package/dist/src/report/writeExperimentReportArtifacts.js +38 -0
- package/dist/src/report/writeReportArtifacts.js +39 -0
- package/dist/src/screenshot/captureReportScreenshot.js +75 -0
- package/dist/src/screenshot/index.js +2 -0
- package/dist/src/screenshot/types.js +1 -0
- package/dist/src/securityValidation/artifacts.js +15 -0
- package/dist/src/securityValidation/cliAdversarial/adversarialCliConfig.js +38 -0
- package/dist/src/securityValidation/cliAdversarial/dataVolumeChecks.js +194 -0
- package/dist/src/securityValidation/cliAdversarial/jsonStdoutChecks.js +359 -0
- package/dist/src/securityValidation/cliAdversarial/malformedArtifactChecks.js +284 -0
- package/dist/src/securityValidation/cliAdversarial/malformedArtifactFixtures.js +79 -0
- package/dist/src/securityValidation/cliAdversarial/pathBoundaryChecks.js +431 -0
- package/dist/src/securityValidation/cliAdversarial/pathCases.js +144 -0
- package/dist/src/securityValidation/cliAdversarial/readOnlyBoundaryChecks.js +294 -0
- package/dist/src/securityValidation/cliAdversarial/runAdversarialCheck.js +149 -0
- package/dist/src/securityValidation/cliAdversarial/subprocessSafetyChecks.js +214 -0
- package/dist/src/securityValidation/cliAdversarial/tempWorkspace.js +160 -0
- package/dist/src/securityValidation/commandRunner.js +136 -0
- package/dist/src/securityValidation/config.js +39 -0
- package/dist/src/securityValidation/dependencies/parseNpmAudit.js +115 -0
- package/dist/src/securityValidation/dependencies/parseNpmLs.js +71 -0
- package/dist/src/securityValidation/dependencies/parseNpmOutdated.js +41 -0
- package/dist/src/securityValidation/dependencies/runDependencyChecks.js +239 -0
- package/dist/src/securityValidation/dependencies/runOsvScanner.js +43 -0
- package/dist/src/securityValidation/fuzz/fuzzHarness.js +61 -0
- package/dist/src/securityValidation/fuzz/fuzzTargets.js +204 -0
- package/dist/src/securityValidation/fuzz/randomInput.js +0 -0
- package/dist/src/securityValidation/index.js +34 -0
- package/dist/src/securityValidation/packageChecks/forbiddenPackageContents.js +67 -0
- package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +56 -0
- package/dist/src/securityValidation/packageChecks/runPackageChecks.js +88 -0
- package/dist/src/securityValidation/report/renderSecurityReport.js +248 -0
- package/dist/src/securityValidation/report/securityReportTypes.js +1 -0
- package/dist/src/securityValidation/staticScans/codeql.js +66 -0
- package/dist/src/securityValidation/staticScans/semgrep.js +180 -0
- package/dist/src/securityValidation/testMatrix.js +535 -0
- package/dist/src/securityValidation/types.js +34 -0
- package/dist/src/securityValidation/validate/resolveTarget.js +32 -0
- package/dist/src/securityValidation/validate/runSecurityValidation.js +169 -0
- package/dist/src/securityValidation/validate/verdict.js +73 -0
- package/dist/src/visualizationDemos/buildMyDevKitVisualizationCommands.js +59 -0
- package/dist/src/visualizationDemos/index.js +4 -0
- package/dist/src/visualizationDemos/runVisualizationDemos.js +82 -0
- package/dist/src/visualizationDemos/types.js +1 -0
- package/dist/src/visualizationDemos/writeVisualizationDemoArtifacts.js +25 -0
- package/docs/METRICS.md +286 -0
- package/examples/demo-report-input.json +78 -0
- package/examples/lab-demo-cases.json +35 -0
- package/examples/real-agent-campaign-cases.json +118 -0
- package/examples/token-savings-cases.json +122 -0
- package/package.json +91 -0
- package/tests/fixtures/fake-adversarial-cli.js +152 -0
- package/tests/fixtures/fake-my-dev-kit-cli.js +83 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Parse the file list from npm pack --dry-run output.
|
|
2
|
+
// npm pack --dry-run does not support --json on all npm versions,
|
|
3
|
+
// so we parse the human-readable text output.
|
|
4
|
+
// Extract the indented file list from npm pack --dry-run text output.
|
|
5
|
+
// Format emitted by npm v7+:
|
|
6
|
+
// npm notice
|
|
7
|
+
// npm notice 📦 my-dev-kit-lab@0.1.0
|
|
8
|
+
// npm notice === Tarball Contents ===
|
|
9
|
+
// npm notice 1.2kB README.md
|
|
10
|
+
// npm notice === Tarball Details ===
|
|
11
|
+
// ...
|
|
12
|
+
export function parseNpmPackDryRun(stdout) {
|
|
13
|
+
if (!stdout.trim()) {
|
|
14
|
+
return { files: [], parseError: "npm pack --dry-run produced no output" };
|
|
15
|
+
}
|
|
16
|
+
const files = [];
|
|
17
|
+
let inContents = false;
|
|
18
|
+
let totalSize;
|
|
19
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
20
|
+
const line = rawLine
|
|
21
|
+
.replace(/^npm notice\s*/i, "")
|
|
22
|
+
.replace(/npm warn.*$/i, "")
|
|
23
|
+
.trim();
|
|
24
|
+
if (/^(?:===\s*)?tarball contents(?:\s*===)?$/i.test(line)) {
|
|
25
|
+
inContents = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (/^(?:===\s*)?tarball details(?:\s*===)?$/i.test(line)) {
|
|
29
|
+
inContents = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (inContents && line) {
|
|
33
|
+
// Strip leading size info (e.g., "1.2kB README.md" → "README.md")
|
|
34
|
+
const match = line.match(/^[\d.]+\s*[kKmMgGbB]+\s+(.+)$/);
|
|
35
|
+
if (match?.[1]) {
|
|
36
|
+
files.push(match[1].trim());
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Fallback: take the whole line if size pattern doesn't match
|
|
40
|
+
files.push(line);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const totalMatch = line.match(/total files\s*:\s*(\d+)/i) ?? line.match(/unpacked size\s*:\s*([\d.]+\s*[kKmMgGbB]+)/i);
|
|
44
|
+
if (totalMatch?.[1]) {
|
|
45
|
+
totalSize = totalMatch[1];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (files.length === 0) {
|
|
49
|
+
return {
|
|
50
|
+
files,
|
|
51
|
+
totalSize,
|
|
52
|
+
parseError: "npm pack --dry-run: no files detected in tarball contents section",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { files, totalSize };
|
|
56
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { runSecurityCommand, resolveNpmCommand } from "../commandRunner.js";
|
|
3
|
+
import { writeCheckResult } from "../artifacts.js";
|
|
4
|
+
import { parseNpmPackDryRun } from "./parseNpmPackDryRun.js";
|
|
5
|
+
import { detectForbiddenContents } from "./forbiddenPackageContents.js";
|
|
6
|
+
// Run npm pack --dry-run and inspect the resulting file list for forbidden contents.
|
|
7
|
+
// Does not publish anything.
|
|
8
|
+
export async function runPackageChecks(options) {
|
|
9
|
+
const { cwd, config } = options;
|
|
10
|
+
const { reportDir, rawOutputDir, commandTimeoutMs, forbiddenPackagePatterns, allowedPackageExceptions } = config;
|
|
11
|
+
const allFindings = [];
|
|
12
|
+
const checks = [];
|
|
13
|
+
const npm = resolveNpmCommand();
|
|
14
|
+
// npm pack --dry-run to get the tarball file list
|
|
15
|
+
const startedAt = new Date().toISOString();
|
|
16
|
+
const cmd = await runSecurityCommand({
|
|
17
|
+
command: npm,
|
|
18
|
+
args: ["pack", "--dry-run"],
|
|
19
|
+
cwd,
|
|
20
|
+
timeoutMs: commandTimeoutMs,
|
|
21
|
+
});
|
|
22
|
+
const finishedAt = new Date().toISOString();
|
|
23
|
+
// npm pack --dry-run writes the tarball filename to stdout on some npm
|
|
24
|
+
// versions and the detailed file list to stderr. Prefer whichever stream
|
|
25
|
+
// contains the tarball contents section, then fall back to combined output.
|
|
26
|
+
const packOutput = [cmd.stdout, cmd.stderr].find((stream) => /tarball contents/i.test(stream)) ??
|
|
27
|
+
[cmd.stdout, cmd.stderr].filter(Boolean).join("\n");
|
|
28
|
+
const parsed = parseNpmPackDryRun(packOutput);
|
|
29
|
+
const { findings: contentFindings } = detectForbiddenContents({
|
|
30
|
+
files: parsed.files,
|
|
31
|
+
forbiddenPatterns: forbiddenPackagePatterns,
|
|
32
|
+
allowedExceptions: allowedPackageExceptions,
|
|
33
|
+
checkId: "npm-pack",
|
|
34
|
+
});
|
|
35
|
+
allFindings.push(...contentFindings);
|
|
36
|
+
const packCheck = {
|
|
37
|
+
id: "npm-pack-dry-run",
|
|
38
|
+
name: "npm pack --dry-run (tarball file list)",
|
|
39
|
+
category: "package-content",
|
|
40
|
+
status: cmd.timedOut
|
|
41
|
+
? "failed"
|
|
42
|
+
: parsed.parseError && parsed.files.length === 0
|
|
43
|
+
? "warning"
|
|
44
|
+
: contentFindings.length > 0
|
|
45
|
+
? "failed"
|
|
46
|
+
: "passed",
|
|
47
|
+
severity: contentFindings.length > 0
|
|
48
|
+
? contentFindings.some((f) => f.severity === "blocker")
|
|
49
|
+
? "blocker"
|
|
50
|
+
: "major"
|
|
51
|
+
: "informational",
|
|
52
|
+
startedAt,
|
|
53
|
+
finishedAt,
|
|
54
|
+
durationMs: cmd.durationMs,
|
|
55
|
+
findings: contentFindings,
|
|
56
|
+
skippedReason: undefined,
|
|
57
|
+
command: "npm pack --dry-run",
|
|
58
|
+
};
|
|
59
|
+
await writeCheckResult({
|
|
60
|
+
result: packCheck,
|
|
61
|
+
outputPath: path.join(reportDir, "npm-pack-dry-run.json"),
|
|
62
|
+
rawDir: rawOutputDir,
|
|
63
|
+
rawStdout: cmd.stdout,
|
|
64
|
+
rawStderr: cmd.stderr,
|
|
65
|
+
});
|
|
66
|
+
checks.push(packCheck);
|
|
67
|
+
// Write combined package checks summary
|
|
68
|
+
const combined = {
|
|
69
|
+
id: "package-checks",
|
|
70
|
+
name: "Package checks summary",
|
|
71
|
+
category: "package-content",
|
|
72
|
+
status: checks.some((c) => c.status === "failed") ? "failed" : checks.some((c) => c.status === "warning") ? "warning" : "passed",
|
|
73
|
+
severity: allFindings.reduce((worst, f) => {
|
|
74
|
+
const order = { blocker: 4, major: 3, minor: 2, informational: 1, skipped: 0 };
|
|
75
|
+
return (order[f.severity] ?? 0) > (order[worst] ?? 0) ? f.severity : worst;
|
|
76
|
+
}, "informational"),
|
|
77
|
+
startedAt: checks[0]?.startedAt ?? new Date().toISOString(),
|
|
78
|
+
finishedAt: checks[checks.length - 1]?.finishedAt ?? new Date().toISOString(),
|
|
79
|
+
durationMs: checks.reduce((sum, c) => sum + c.durationMs, 0),
|
|
80
|
+
findings: allFindings,
|
|
81
|
+
};
|
|
82
|
+
await writeCheckResult({
|
|
83
|
+
result: combined,
|
|
84
|
+
outputPath: path.join(reportDir, "package-checks.json"),
|
|
85
|
+
rawDir: rawOutputDir,
|
|
86
|
+
});
|
|
87
|
+
return { checks, findings: allFindings };
|
|
88
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { verdictToHumanLabel } from "../validate/verdict.js";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Human-readable text report
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
function pad(label, width = 36) {
|
|
6
|
+
return label.padEnd(width, " ");
|
|
7
|
+
}
|
|
8
|
+
function statusIcon(status) {
|
|
9
|
+
switch (status) {
|
|
10
|
+
case "passed": return "PASS";
|
|
11
|
+
case "failed": return "FAIL";
|
|
12
|
+
case "warning": return "WARN";
|
|
13
|
+
case "skipped": return "SKIP";
|
|
14
|
+
default: return " ";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function formatDuration(ms) {
|
|
18
|
+
if (ms < 1000)
|
|
19
|
+
return `${ms}ms`;
|
|
20
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
21
|
+
}
|
|
22
|
+
function divider(char = "-", width = 72) {
|
|
23
|
+
return char.repeat(width);
|
|
24
|
+
}
|
|
25
|
+
export function renderTextReport(report) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
const { metadata, allChecks, allFindings, verdict, recommendedNextStep } = report;
|
|
28
|
+
lines.push(divider("="));
|
|
29
|
+
lines.push("SECURITY VALIDATION REPORT");
|
|
30
|
+
lines.push(divider("="));
|
|
31
|
+
if (!metadata.isSelf) {
|
|
32
|
+
lines.push(`Tool : ${metadata.toolPackageName}@${metadata.toolPackageVersion}`);
|
|
33
|
+
lines.push(`Tool root : ${metadata.toolRoot}`);
|
|
34
|
+
lines.push(`Target : ${metadata.packageName}@${metadata.packageVersion}`);
|
|
35
|
+
lines.push(`Target root: ${metadata.targetRoot}`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
lines.push(`Package : ${metadata.packageName}@${metadata.packageVersion}`);
|
|
39
|
+
}
|
|
40
|
+
lines.push(`Branch : ${metadata.branch}`);
|
|
41
|
+
lines.push(`Commit : ${metadata.commit}`);
|
|
42
|
+
lines.push(`Generated : ${metadata.generatedAt}`);
|
|
43
|
+
lines.push(`Duration : ${formatDuration(metadata.totalDurationMs)}`);
|
|
44
|
+
lines.push("");
|
|
45
|
+
// --- 1. Executive Summary ---
|
|
46
|
+
lines.push(divider());
|
|
47
|
+
lines.push("1. EXECUTIVE SUMMARY");
|
|
48
|
+
lines.push(divider());
|
|
49
|
+
lines.push(`Verdict : ${verdictToHumanLabel(verdict).toUpperCase()}`);
|
|
50
|
+
lines.push(`Checks run : ${allChecks.length}`);
|
|
51
|
+
lines.push(` Passed : ${allChecks.filter((c) => c.status === "passed").length}`);
|
|
52
|
+
lines.push(` Warned : ${allChecks.filter((c) => c.status === "warning").length}`);
|
|
53
|
+
lines.push(` Failed : ${allChecks.filter((c) => c.status === "failed").length}`);
|
|
54
|
+
lines.push(` Skipped : ${allChecks.filter((c) => c.status === "skipped").length}`);
|
|
55
|
+
lines.push(`Findings : ${allFindings.length}`);
|
|
56
|
+
if (allFindings.length > 0) {
|
|
57
|
+
for (const sev of ["blocker", "major", "minor", "informational"]) {
|
|
58
|
+
const count = allFindings.filter((f) => f.severity === sev).length;
|
|
59
|
+
if (count > 0)
|
|
60
|
+
lines.push(` ${sev.padEnd(16)} : ${count}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
lines.push(`Next step : ${recommendedNextStep}`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
// --- Check sections ---
|
|
66
|
+
const sectionDefs = [
|
|
67
|
+
{ num: 2, title: "Branch and Commit", ids: [], alwaysShow: true },
|
|
68
|
+
{ num: 3, title: "Package Name and Version", ids: [], alwaysShow: true },
|
|
69
|
+
{ num: 4, title: "Security Model", ids: [], alwaysShow: true },
|
|
70
|
+
{ num: 5, title: "CodeQL Result", ids: ["codeql-scan"] },
|
|
71
|
+
{ num: 6, title: "Semgrep Result", ids: ["semgrep-scan"] },
|
|
72
|
+
{ num: 7, title: "npm audit Result", ids: ["npm-audit-full", "npm-audit-runtime"] },
|
|
73
|
+
{ num: 8, title: "OSV-Scanner Result", ids: ["osv-scanner"] },
|
|
74
|
+
{ num: 9, title: "Package Tarball Inspection", ids: ["npm-pack-dry-run"] },
|
|
75
|
+
{ num: 10, title: "File-System / Path Traversal Tests", ids: ["path-traversal-root", "path-traversal-out", "path-traversal-index", "absolute-path-escape", "path-harness-escape-detection"] },
|
|
76
|
+
{ num: 11, title: "Source Read-Only Boundary Tests", ids: ["source-files-not-modified", "writes-limited-to-output", "index-write-containment", "artifact-cleanup-safe"] },
|
|
77
|
+
{ num: 12, title: "Graphviz / Subprocess Safety Tests", ids: ["graphviz-label-escaping", "subprocess-no-shell-interpolation"] },
|
|
78
|
+
{ num: 13, title: "JSON stdout/stderr Safety Tests", ids: ["json-mode-parseable-output", "stderr-not-in-stdout", "json-failure-error-object", "progress-not-in-json-stdout"] },
|
|
79
|
+
{ num: 14, title: "Network Boundary Check", ids: [] },
|
|
80
|
+
{ num: 15, title: "Secret Leakage Check", ids: [] },
|
|
81
|
+
{ num: 16, title: "Artifact Content Safety Check", ids: ["artifact-cleanup-safe"] },
|
|
82
|
+
{ num: 17, title: "Symlink / Ignored-Folder Behavior", ids: [] },
|
|
83
|
+
{ num: 18, title: "Invalid Input / Error-Message Behavior", ids: ["malformed-manifest-all-cases", "malformed-code-graph", "unsupported-schema-version", "missing-index-directory"] },
|
|
84
|
+
{ num: 19, title: "Fuzz Smoke Result", ids: ["fuzz-smoke"] },
|
|
85
|
+
];
|
|
86
|
+
for (const def of sectionDefs) {
|
|
87
|
+
lines.push(divider());
|
|
88
|
+
lines.push(`${def.num}. ${def.title.toUpperCase()}`);
|
|
89
|
+
lines.push(divider());
|
|
90
|
+
if (def.num === 2) {
|
|
91
|
+
lines.push(`Branch : ${metadata.branch}`);
|
|
92
|
+
lines.push(`Commit : ${metadata.commit}`);
|
|
93
|
+
lines.push("");
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (def.num === 3) {
|
|
97
|
+
lines.push(`Name : ${metadata.packageName}`);
|
|
98
|
+
lines.push(`Version : ${metadata.packageVersion}`);
|
|
99
|
+
lines.push("");
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (def.num === 4) {
|
|
103
|
+
lines.push("my-dev-kit is a local CLI package. The security model covers:");
|
|
104
|
+
lines.push(" - local-first: no cloud/network calls during normal operation");
|
|
105
|
+
lines.push(" - deterministic: reproducible outputs from same inputs");
|
|
106
|
+
lines.push(" - read-only: does not modify user source files");
|
|
107
|
+
lines.push(" - network-free: normal CLI operation requires no network");
|
|
108
|
+
lines.push(" - LLM-free: no LLM calls");
|
|
109
|
+
lines.push(" - database-free: no external databases");
|
|
110
|
+
lines.push("This is CLI/package adversarial testing, not web-app pentesting.");
|
|
111
|
+
lines.push("");
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (def.num === 14) {
|
|
115
|
+
lines.push("Normal my-dev-kit CLI operation does not require network access.");
|
|
116
|
+
lines.push("Network access is only expected for npm lifecycle commands.");
|
|
117
|
+
lines.push("Status: INFORMATIONAL (architectural guarantee, not tested dynamically)");
|
|
118
|
+
lines.push("");
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (def.num === 15) {
|
|
122
|
+
lines.push("Artifact content is validated by the package tarball inspection check.");
|
|
123
|
+
lines.push("Environment variables are not serialized into generated artifacts.");
|
|
124
|
+
lines.push("Status: INFORMATIONAL (covered by package content checks)");
|
|
125
|
+
lines.push("");
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (def.num === 17) {
|
|
129
|
+
lines.push("Symlink/junction escape tests are environment-dependent (require OS support).");
|
|
130
|
+
lines.push("Status: SKIPPED-ENVIRONMENT (marked in test matrix)");
|
|
131
|
+
lines.push("");
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (def.ids.length === 0) {
|
|
135
|
+
lines.push("(No dedicated checks for this section in this run.)");
|
|
136
|
+
lines.push("");
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const sectionChecks = allChecks.filter((c) => def.ids.includes(c.id));
|
|
140
|
+
if (sectionChecks.length === 0) {
|
|
141
|
+
lines.push("(Checks not included in this run.)");
|
|
142
|
+
lines.push("");
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
for (const check of sectionChecks) {
|
|
146
|
+
lines.push(`[${statusIcon(check.status)}] ${pad(check.name)} ${formatDuration(check.durationMs)}`);
|
|
147
|
+
if (check.status === "skipped" && check.skippedReason) {
|
|
148
|
+
lines.push(` Reason: ${check.skippedReason}`);
|
|
149
|
+
}
|
|
150
|
+
if (check.findings.length > 0) {
|
|
151
|
+
for (const f of check.findings) {
|
|
152
|
+
lines.push(` [${f.severity.toUpperCase()}] ${f.title}`);
|
|
153
|
+
if (f.description)
|
|
154
|
+
lines.push(` ${f.description.slice(0, 120)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
lines.push("");
|
|
159
|
+
}
|
|
160
|
+
// --- 20. Findings by Severity ---
|
|
161
|
+
lines.push(divider());
|
|
162
|
+
lines.push("20. FINDINGS BY SEVERITY");
|
|
163
|
+
lines.push(divider());
|
|
164
|
+
if (allFindings.length === 0) {
|
|
165
|
+
lines.push("No findings.");
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
for (const sev of ["blocker", "major", "minor", "informational"]) {
|
|
169
|
+
const sevFindings = allFindings.filter((f) => f.severity === sev);
|
|
170
|
+
if (sevFindings.length === 0)
|
|
171
|
+
continue;
|
|
172
|
+
lines.push(`${sev.toUpperCase()} (${sevFindings.length}):`);
|
|
173
|
+
for (const f of sevFindings) {
|
|
174
|
+
lines.push(` [${f.id}] ${f.title}`);
|
|
175
|
+
if (f.description)
|
|
176
|
+
lines.push(` ${f.description.slice(0, 120)}`);
|
|
177
|
+
if (f.recommendation)
|
|
178
|
+
lines.push(` Recommendation: ${f.recommendation}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
lines.push("");
|
|
183
|
+
// --- 21. Release Verdict ---
|
|
184
|
+
lines.push(divider("="));
|
|
185
|
+
lines.push("21. RELEASE VERDICT");
|
|
186
|
+
lines.push(divider("="));
|
|
187
|
+
lines.push(verdictToHumanLabel(verdict).toUpperCase());
|
|
188
|
+
lines.push("");
|
|
189
|
+
// --- 22. Recommended Next Step ---
|
|
190
|
+
lines.push(divider());
|
|
191
|
+
lines.push("22. RECOMMENDED NEXT STEP");
|
|
192
|
+
lines.push(divider());
|
|
193
|
+
lines.push(recommendedNextStep);
|
|
194
|
+
lines.push("");
|
|
195
|
+
lines.push(divider("="));
|
|
196
|
+
return lines.join("\n");
|
|
197
|
+
}
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// JSON report
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
export function renderJsonReport(report) {
|
|
202
|
+
const { metadata, allChecks, allFindings, verdict, recommendedNextStep } = report;
|
|
203
|
+
const sanitizedChecks = allChecks.map((c) => ({
|
|
204
|
+
id: c.id,
|
|
205
|
+
name: c.name,
|
|
206
|
+
category: c.category,
|
|
207
|
+
status: c.status,
|
|
208
|
+
severity: c.severity,
|
|
209
|
+
startedAt: c.startedAt,
|
|
210
|
+
finishedAt: c.finishedAt,
|
|
211
|
+
durationMs: c.durationMs,
|
|
212
|
+
findingCount: c.findings.length,
|
|
213
|
+
skippedReason: c.skippedReason ?? null,
|
|
214
|
+
command: c.command ?? null,
|
|
215
|
+
}));
|
|
216
|
+
const sanitizedFindings = allFindings.map((f) => ({
|
|
217
|
+
id: f.id,
|
|
218
|
+
title: f.title,
|
|
219
|
+
severity: f.severity,
|
|
220
|
+
category: f.category,
|
|
221
|
+
description: f.description,
|
|
222
|
+
evidence: f.evidence ? f.evidence.slice(0, 500) : undefined,
|
|
223
|
+
affectedFiles: f.affectedFiles ?? [],
|
|
224
|
+
recommendation: f.recommendation ?? undefined,
|
|
225
|
+
releaseImpact: f.releaseImpact,
|
|
226
|
+
}));
|
|
227
|
+
const output = {
|
|
228
|
+
schemaVersion: 1,
|
|
229
|
+
metadata,
|
|
230
|
+
summary: {
|
|
231
|
+
totalChecks: allChecks.length,
|
|
232
|
+
passed: allChecks.filter((c) => c.status === "passed").length,
|
|
233
|
+
warned: allChecks.filter((c) => c.status === "warning").length,
|
|
234
|
+
failed: allChecks.filter((c) => c.status === "failed").length,
|
|
235
|
+
skipped: allChecks.filter((c) => c.status === "skipped").length,
|
|
236
|
+
totalFindings: allFindings.length,
|
|
237
|
+
blockerFindings: allFindings.filter((f) => f.severity === "blocker").length,
|
|
238
|
+
majorFindings: allFindings.filter((f) => f.severity === "major").length,
|
|
239
|
+
minorFindings: allFindings.filter((f) => f.severity === "minor").length,
|
|
240
|
+
},
|
|
241
|
+
verdict,
|
|
242
|
+
verdictLabel: verdictToHumanLabel(verdict),
|
|
243
|
+
recommendedNextStep,
|
|
244
|
+
checks: sanitizedChecks,
|
|
245
|
+
findings: sanitizedFindings,
|
|
246
|
+
};
|
|
247
|
+
return JSON.stringify(output, null, 2);
|
|
248
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { runSecurityCommand } from "../commandRunner.js";
|
|
2
|
+
import { resolveCommand } from "../../core/resolveCommand.js";
|
|
3
|
+
import { skippedCheck } from "../cliAdversarial/runAdversarialCheck.js";
|
|
4
|
+
// CodeQL is primarily a GitHub Actions / code-scanning integration.
|
|
5
|
+
// Local CodeQL CLI is optional — if unavailable the check is skipped.
|
|
6
|
+
// Absence is not a release blocker; it surfaces as "ready except optional checks".
|
|
7
|
+
export async function runCodeqlCheck(options) {
|
|
8
|
+
const { cwd, timeoutMs } = options;
|
|
9
|
+
const resolved = resolveCommand("codeql", { cwd, env: process.env });
|
|
10
|
+
if (resolved.resolutionKind === "unavailable") {
|
|
11
|
+
return skippedCheck({
|
|
12
|
+
id: "codeql-scan",
|
|
13
|
+
name: "CodeQL static analysis",
|
|
14
|
+
category: "static-scan",
|
|
15
|
+
reason: "CodeQL CLI not found in PATH. Full analysis runs via GitHub Actions code-scanning workflow. Install the CodeQL CLI locally for local analysis.",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const startedAt = new Date().toISOString();
|
|
19
|
+
// Confirm the CLI is functional with a version check.
|
|
20
|
+
// Full database creation and analysis is delegated to GitHub Actions because
|
|
21
|
+
// it requires a build step and significant disk/CPU resources.
|
|
22
|
+
const versionCmd = await runSecurityCommand({
|
|
23
|
+
command: "codeql",
|
|
24
|
+
args: ["version", "--format", "terse"],
|
|
25
|
+
cwd,
|
|
26
|
+
timeoutMs: Math.min(timeoutMs, 15_000),
|
|
27
|
+
});
|
|
28
|
+
const finishedAt = new Date().toISOString();
|
|
29
|
+
if (versionCmd.exitCode !== 0 || versionCmd.exitCode === null) {
|
|
30
|
+
return {
|
|
31
|
+
id: "codeql-scan",
|
|
32
|
+
name: "CodeQL static analysis",
|
|
33
|
+
category: "static-scan",
|
|
34
|
+
status: "failed",
|
|
35
|
+
severity: "major",
|
|
36
|
+
startedAt,
|
|
37
|
+
finishedAt,
|
|
38
|
+
durationMs: versionCmd.durationMs,
|
|
39
|
+
findings: [
|
|
40
|
+
{
|
|
41
|
+
id: "codeql-cli-error",
|
|
42
|
+
title: "CodeQL CLI returned an error",
|
|
43
|
+
severity: "major",
|
|
44
|
+
category: "static-scan",
|
|
45
|
+
description: `CodeQL CLI exited with code ${String(versionCmd.exitCode)}. stderr: ${versionCmd.stderr.slice(0, 500)}`,
|
|
46
|
+
recommendation: "Verify CodeQL CLI installation.",
|
|
47
|
+
releaseImpact: "Review before release",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
command: "codeql version --format terse",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// CLI is present and functional. Full analysis runs in GitHub Actions.
|
|
54
|
+
return {
|
|
55
|
+
id: "codeql-scan",
|
|
56
|
+
name: "CodeQL static analysis",
|
|
57
|
+
category: "static-scan",
|
|
58
|
+
status: "passed",
|
|
59
|
+
severity: "informational",
|
|
60
|
+
startedAt,
|
|
61
|
+
finishedAt,
|
|
62
|
+
durationMs: versionCmd.durationMs,
|
|
63
|
+
findings: [],
|
|
64
|
+
command: "codeql version --format terse",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { runSecurityCommand } from "../commandRunner.js";
|
|
4
|
+
import { resolveCommand } from "../../core/resolveCommand.js";
|
|
5
|
+
import { skippedCheck } from "../cliAdversarial/runAdversarialCheck.js";
|
|
6
|
+
export function parseSemgrepJson(raw) {
|
|
7
|
+
if (!raw.trim()) {
|
|
8
|
+
return { findings: [], rawOutput: raw };
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
const results = Array.isArray(parsed["results"]) ? parsed["results"] : [];
|
|
13
|
+
const findings = results.map((r) => {
|
|
14
|
+
const item = r;
|
|
15
|
+
const checkId = typeof item["check_id"] === "string" ? item["check_id"] : "unknown";
|
|
16
|
+
const extra = item["extra"] ?? {};
|
|
17
|
+
const metadata = extra["metadata"] ?? {};
|
|
18
|
+
const severity = typeof extra["severity"] === "string"
|
|
19
|
+
? extra["severity"]
|
|
20
|
+
: typeof metadata["severity"] === "string"
|
|
21
|
+
? metadata["severity"]
|
|
22
|
+
: "WARNING";
|
|
23
|
+
const message = typeof extra["message"] === "string" ? extra["message"] : checkId;
|
|
24
|
+
const filePath = typeof item["path"] === "string" ? item["path"] : "";
|
|
25
|
+
const startObj = item["start"] ?? {};
|
|
26
|
+
const line = typeof startObj["line"] === "number" ? startObj["line"] : 0;
|
|
27
|
+
return { ruleId: checkId, severity, message, path: filePath, line };
|
|
28
|
+
});
|
|
29
|
+
return { findings, rawOutput: raw };
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
return {
|
|
33
|
+
findings: [],
|
|
34
|
+
parseError: err instanceof Error ? err.message : String(err),
|
|
35
|
+
rawOutput: raw,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function semgrepSeverityToSecurity(semgrepSeverity) {
|
|
40
|
+
switch (semgrepSeverity.toUpperCase()) {
|
|
41
|
+
case "ERROR":
|
|
42
|
+
return "major";
|
|
43
|
+
case "WARNING":
|
|
44
|
+
return "minor";
|
|
45
|
+
default:
|
|
46
|
+
return "informational";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function runSemgrepCheck(options) {
|
|
50
|
+
const { targetRoot, timeoutMs } = options;
|
|
51
|
+
const toolRoot = options.toolRoot ?? targetRoot;
|
|
52
|
+
const configPath = options.configPath ?? path.join(toolRoot, ".semgrep.yml");
|
|
53
|
+
// Prefer a locally installed semgrep binary; fall back to npx.
|
|
54
|
+
const localResolved = resolveCommand("semgrep", { cwd: toolRoot, env: process.env });
|
|
55
|
+
const useNpx = localResolved.resolutionKind === "unavailable";
|
|
56
|
+
// Check if npx is available as fallback.
|
|
57
|
+
if (useNpx) {
|
|
58
|
+
const npxResolved = resolveCommand("npx", { cwd: toolRoot, env: process.env });
|
|
59
|
+
if (npxResolved.resolutionKind === "unavailable") {
|
|
60
|
+
return skippedCheck({
|
|
61
|
+
id: "semgrep-scan",
|
|
62
|
+
name: "Semgrep static analysis",
|
|
63
|
+
category: "static-scan",
|
|
64
|
+
reason: "Semgrep CLI not found in PATH and npx is unavailable. Install semgrep (pip install semgrep) or ensure npx is on PATH.",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const startedAt = new Date().toISOString();
|
|
69
|
+
// Determine what directory to scan.
|
|
70
|
+
// For self-validation, scan src/ (matches original behavior).
|
|
71
|
+
// For external targets, scan src/ if it exists, otherwise the target root itself.
|
|
72
|
+
const isSelf = path.resolve(targetRoot) === path.resolve(toolRoot);
|
|
73
|
+
const srcDir = path.join(targetRoot, "src");
|
|
74
|
+
let scanTarget;
|
|
75
|
+
if (isSelf) {
|
|
76
|
+
scanTarget = "src/";
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
scanTarget = fs.existsSync(srcDir) ? srcDir : targetRoot;
|
|
80
|
+
}
|
|
81
|
+
const command = useNpx ? "npx" : "semgrep";
|
|
82
|
+
const baseArgs = useNpx ? ["--yes", "semgrep"] : [];
|
|
83
|
+
const scanArgs = [
|
|
84
|
+
...baseArgs,
|
|
85
|
+
"scan",
|
|
86
|
+
"--config", configPath,
|
|
87
|
+
"--json",
|
|
88
|
+
"--quiet",
|
|
89
|
+
scanTarget,
|
|
90
|
+
];
|
|
91
|
+
const cmd = await runSecurityCommand({
|
|
92
|
+
command,
|
|
93
|
+
args: scanArgs,
|
|
94
|
+
cwd: targetRoot,
|
|
95
|
+
timeoutMs,
|
|
96
|
+
});
|
|
97
|
+
const finishedAt = new Date().toISOString();
|
|
98
|
+
// Semgrep exits 0 = no findings, 1 = findings found, other = error.
|
|
99
|
+
const isError = cmd.exitCode !== 0 && cmd.exitCode !== 1;
|
|
100
|
+
const isTimedOut = cmd.timedOut;
|
|
101
|
+
if (isTimedOut || (isError && !cmd.stdout.trim())) {
|
|
102
|
+
return {
|
|
103
|
+
id: "semgrep-scan",
|
|
104
|
+
name: "Semgrep static analysis",
|
|
105
|
+
category: "static-scan",
|
|
106
|
+
status: "failed",
|
|
107
|
+
severity: "major",
|
|
108
|
+
startedAt,
|
|
109
|
+
finishedAt,
|
|
110
|
+
durationMs: cmd.durationMs,
|
|
111
|
+
findings: [
|
|
112
|
+
{
|
|
113
|
+
id: "semgrep-execution-error",
|
|
114
|
+
title: "Semgrep execution failed",
|
|
115
|
+
severity: "major",
|
|
116
|
+
category: "static-scan",
|
|
117
|
+
description: isTimedOut
|
|
118
|
+
? "Semgrep timed out."
|
|
119
|
+
: `Semgrep exited with code ${String(cmd.exitCode)}. stderr: ${cmd.stderr.slice(0, 500)}`,
|
|
120
|
+
recommendation: "Check semgrep installation and .semgrep.yml config.",
|
|
121
|
+
releaseImpact: "Review before release",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
command: [command, ...scanArgs].join(" "),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const parsed = parseSemgrepJson(cmd.stdout);
|
|
128
|
+
if (parsed.parseError) {
|
|
129
|
+
return {
|
|
130
|
+
id: "semgrep-scan",
|
|
131
|
+
name: "Semgrep static analysis",
|
|
132
|
+
category: "static-scan",
|
|
133
|
+
status: "warning",
|
|
134
|
+
severity: "minor",
|
|
135
|
+
startedAt,
|
|
136
|
+
finishedAt,
|
|
137
|
+
durationMs: cmd.durationMs,
|
|
138
|
+
findings: [
|
|
139
|
+
{
|
|
140
|
+
id: "semgrep-parse-error",
|
|
141
|
+
title: "Could not parse Semgrep output",
|
|
142
|
+
severity: "minor",
|
|
143
|
+
category: "static-scan",
|
|
144
|
+
description: `JSON parse error: ${parsed.parseError}`,
|
|
145
|
+
recommendation: "Verify semgrep version produces valid JSON output.",
|
|
146
|
+
releaseImpact: "Review before release",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
command: [command, ...scanArgs].join(" "),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const securityFindings = parsed.findings.map((f) => ({
|
|
153
|
+
id: `semgrep-${f.ruleId.replace(/[^a-z0-9-]/gi, "-")}`,
|
|
154
|
+
title: f.ruleId,
|
|
155
|
+
severity: semgrepSeverityToSecurity(f.severity),
|
|
156
|
+
category: "static-scan",
|
|
157
|
+
description: f.message,
|
|
158
|
+
affectedFiles: [`${f.path}:${f.line}`],
|
|
159
|
+
releaseImpact: semgrepSeverityToSecurity(f.severity) === "major"
|
|
160
|
+
? "Should fix before release"
|
|
161
|
+
: "Review before release",
|
|
162
|
+
}));
|
|
163
|
+
const hasBlockerOrMajor = securityFindings.some((f) => f.severity === "blocker" || f.severity === "major");
|
|
164
|
+
return {
|
|
165
|
+
id: "semgrep-scan",
|
|
166
|
+
name: "Semgrep static analysis",
|
|
167
|
+
category: "static-scan",
|
|
168
|
+
status: securityFindings.length === 0 ? "passed" : hasBlockerOrMajor ? "failed" : "warning",
|
|
169
|
+
severity: securityFindings.length === 0
|
|
170
|
+
? "informational"
|
|
171
|
+
: hasBlockerOrMajor
|
|
172
|
+
? "major"
|
|
173
|
+
: "minor",
|
|
174
|
+
startedAt,
|
|
175
|
+
finishedAt,
|
|
176
|
+
durationMs: cmd.durationMs,
|
|
177
|
+
findings: securityFindings,
|
|
178
|
+
command: [command, ...scanArgs].join(" "),
|
|
179
|
+
};
|
|
180
|
+
}
|