@dailephd/my-dev-kit-lab 0.9.2 → 0.9.4
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/CHANGELOG.md +17 -1
- package/README.md +9 -5
- package/dist/src/agents/adapters/codexAdapter.js +4 -1
- package/dist/src/cli/help.js +38 -2
- package/dist/src/cli/runLabCli.js +9 -1
- package/dist/src/commands/buildGalleryCommand.js +16 -3
- package/dist/src/commands/runComparePromptsCommand.js +109 -0
- package/dist/src/commands/runExperimentRunCommand.js +47 -2
- package/dist/src/core/processTree.js +41 -17
- package/dist/src/core/runMeasuredCommand.js +14 -8
- package/dist/src/evaluation/affectedNeighborhood.js +140 -32
- package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +23 -1
- package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +7 -2
- package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +29 -4
- package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +1 -1
- package/dist/src/experiments/plugins/agentSuccessRate/config.js +47 -2
- package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +4 -2
- package/dist/src/experiments/plugins/agentSuccessRate/promptContract.js +121 -0
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +32 -2
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +105 -11
- package/dist/src/experiments/plugins/agentSuccessRate/strictDiffResponse.js +312 -0
- package/dist/src/gallery/galleryPathSafety.js +111 -0
- package/dist/src/gallery/readGallerySources.js +542 -0
- package/dist/src/gallery/writeGalleryManifest.js +57 -3
- package/dist/src/report/experiments/buildPluginExperimentReport.js +4 -16
- package/dist/src/report/experiments/htmlSafety.js +32 -0
- package/dist/src/report/experiments/metricEvidence.js +389 -0
- package/dist/src/report/experiments/metricGlossary.js +227 -0
- package/dist/src/report/experiments/metricGlossaryCatalog.js +199 -0
- package/dist/src/report/experiments/promptTemplateComparisonEvidence.js +377 -0
- package/dist/src/report/experiments/promptTemplateComparisonModel.js +436 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +158 -64
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +22 -677
- package/dist/src/report/experiments/renderPromptTemplateComparison.js +192 -0
- package/dist/src/report/experiments/renderSpecializedTextSections.js +633 -0
- package/dist/src/report/experiments/reportSectionRegistry.js +79 -0
- package/dist/src/report/experiments/specializedReportSections.js +142 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +4 -2
- package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +146 -133
- package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +54 -36
- package/dist/src/securityValidation/commandRunner.js +13 -6
- package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +46 -21
- package/docs/METRICS.md +11 -1
- package/package.json +3 -3
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Shared, typed registry of specialized (experiment-family) report sections. Core report sections
|
|
2
|
+
// (metadata, variants, cases, metrics, warnings, interpretation, artifacts) stay in the generic renderers.
|
|
3
|
+
// The registry only selects, orders and dispatches specialized sections for the builder, text and HTML renderers.
|
|
4
|
+
// It performs no calculation, no I/O, no dynamic import, and registers only in-process descriptors.
|
|
5
|
+
const ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
6
|
+
function validate(descriptor) {
|
|
7
|
+
const label = typeof descriptor?.id === "string" ? descriptor.id : "<unknown>";
|
|
8
|
+
if (typeof descriptor?.id !== "string" || !ID_PATTERN.test(descriptor.id)) {
|
|
9
|
+
throw new Error(`Invalid report section registration ${label}: id must be a stable lowercase namespaced identifier.`);
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isInteger(descriptor.order)) {
|
|
12
|
+
throw new Error(`Invalid report section registration ${label}: order must be an integer.`);
|
|
13
|
+
}
|
|
14
|
+
if (typeof descriptor.projectionKey !== "string" || descriptor.projectionKey.length === 0) {
|
|
15
|
+
throw new Error(`Invalid report section registration ${label}: projectionKey is required.`);
|
|
16
|
+
}
|
|
17
|
+
const pluginIds = descriptor.appliesTo?.pluginIds;
|
|
18
|
+
if (!Array.isArray(pluginIds) || pluginIds.length === 0 || pluginIds.some((id) => typeof id !== "string" || !id)) {
|
|
19
|
+
throw new Error(`Invalid report section registration ${label}: appliesTo.pluginIds must be a non-empty list of plugin ids.`);
|
|
20
|
+
}
|
|
21
|
+
for (const member of ["build", "renderText", "renderHtml"]) {
|
|
22
|
+
if (typeof descriptor[member] !== "function") {
|
|
23
|
+
throw new Error(`Invalid report section registration ${label}: ${member} must be a function.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (descriptor.text?.whenAbsent !== "placeholder" && descriptor.text?.whenAbsent !== "omit") {
|
|
27
|
+
throw new Error(`Invalid report section registration ${label}: text.whenAbsent must be placeholder or omit.`);
|
|
28
|
+
}
|
|
29
|
+
if (descriptor.text.whenAbsent === "placeholder" && !descriptor.text.title) {
|
|
30
|
+
throw new Error(`Invalid report section registration ${label}: a placeholder text section requires a title.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Assembles an immutable registry. Duplicate ids, duplicate projection keys and invalid descriptors fail. */
|
|
34
|
+
export function createReportSectionRegistry(input) {
|
|
35
|
+
const ids = new Set();
|
|
36
|
+
const keys = new Set();
|
|
37
|
+
const frozen = [];
|
|
38
|
+
for (const descriptor of input) {
|
|
39
|
+
validate(descriptor);
|
|
40
|
+
if (ids.has(descriptor.id)) {
|
|
41
|
+
throw new Error(`Duplicate report section registration: ${descriptor.id}.`);
|
|
42
|
+
}
|
|
43
|
+
if (keys.has(descriptor.projectionKey)) {
|
|
44
|
+
throw new Error(`Duplicate report section projection key ${descriptor.projectionKey} (section ${descriptor.id}).`);
|
|
45
|
+
}
|
|
46
|
+
ids.add(descriptor.id);
|
|
47
|
+
keys.add(descriptor.projectionKey);
|
|
48
|
+
frozen.push(Object.freeze({
|
|
49
|
+
...descriptor,
|
|
50
|
+
appliesTo: Object.freeze({ pluginIds: Object.freeze([...descriptor.appliesTo.pluginIds]) }),
|
|
51
|
+
text: Object.freeze({ ...descriptor.text }),
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
frozen.sort((a, b) => a.order - b.order || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
55
|
+
const descriptors = Object.freeze(frozen);
|
|
56
|
+
const byId = new Map(descriptors.map((descriptor) => [descriptor.id, descriptor]));
|
|
57
|
+
const availabilityOf = (descriptor, report) => {
|
|
58
|
+
const projection = report[descriptor.projectionKey] ?? null;
|
|
59
|
+
if (projection !== null)
|
|
60
|
+
return "available";
|
|
61
|
+
return descriptor.appliesTo.pluginIds.includes(report.plugin.id) ? "unavailable" : "not-applicable";
|
|
62
|
+
};
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
descriptors,
|
|
65
|
+
get: (id) => byId.get(id),
|
|
66
|
+
resolve: (report) => descriptors.map((descriptor) => ({
|
|
67
|
+
id: descriptor.id,
|
|
68
|
+
order: descriptor.order,
|
|
69
|
+
availability: availabilityOf(descriptor, report),
|
|
70
|
+
descriptor,
|
|
71
|
+
})),
|
|
72
|
+
resolveById: (id, report) => {
|
|
73
|
+
const descriptor = byId.get(id);
|
|
74
|
+
if (!descriptor)
|
|
75
|
+
return { id, order: Number.MAX_SAFE_INTEGER, availability: "unsupported", descriptor: null };
|
|
76
|
+
return { id, order: descriptor.order, availability: availabilityOf(descriptor, report), descriptor };
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// The registered specialized report families. Each descriptor adapts an existing family builder and
|
|
2
|
+
// renderer; no builder or renderer is cloned here. Order values reproduce the established section order
|
|
3
|
+
// (plugin families are mutually exclusive, so at most one family projection is present for a given run).
|
|
4
|
+
import { AGENT_SUCCESS_RATE_PLUGIN_ID } from "../../experiments/plugins/agentSuccessRate/metadata.js";
|
|
5
|
+
import { CONTEXT_PACK_GENERATION_PLUGIN_ID } from "../../experiments/plugins/contextPackGeneration/metadata.js";
|
|
6
|
+
import { CONTEXT_WINDOW_SCALING_PLUGIN_ID } from "../../experiments/plugins/contextWindowScaling/metadata.js";
|
|
7
|
+
import { INCREMENTAL_CHANGE_STALENESS_PLUGIN_ID } from "../../experiments/plugins/incrementalChangeStaleness/plugin.js";
|
|
8
|
+
import { RETRIEVAL_PRECISION_RECALL_PLUGIN_ID } from "../../experiments/plugins/retrievalPrecisionRecall/metadata.js";
|
|
9
|
+
import { RETRIEVAL_QUERY_STRATEGY_COMPARISON_PLUGIN_ID } from "../../experiments/plugins/retrievalQueryStrategyComparison/metadata.js";
|
|
10
|
+
import { buildAgentSuccessRateReport } from "./buildAgentSuccessRateReport.js";
|
|
11
|
+
import { buildContextPackGenerationReport } from "./buildContextPackGenerationReport.js";
|
|
12
|
+
import { buildContextStrategyComparisonV043Report } from "./buildContextStrategyComparisonV043Report.js";
|
|
13
|
+
import { buildContextWindowScalingReport } from "./buildContextWindowScalingReport.js";
|
|
14
|
+
import { buildIncrementalChangeStalenessPluginReport } from "./buildIncrementalChangeStalenessPluginReport.js";
|
|
15
|
+
import { buildRetrievalPrecisionRecallReport } from "./buildRetrievalPrecisionRecallReport.js";
|
|
16
|
+
import { buildRetrievalQueryStrategyComparisonReport } from "./buildRetrievalQueryStrategyComparisonReport.js";
|
|
17
|
+
import { buildWarmIndexReuseReport } from "./buildWarmIndexReuseReport.js";
|
|
18
|
+
import { renderAgentSuccessRateHtml } from "./renderAgentSuccessRateHtml.js";
|
|
19
|
+
import { renderAgentSuccessRateTextLines } from "./renderAgentSuccessRateText.js";
|
|
20
|
+
import { renderContextPackGenerationHtml } from "./renderContextPackGenerationHtml.js";
|
|
21
|
+
import { renderContextPackGenerationTextLines } from "./renderContextPackGenerationText.js";
|
|
22
|
+
import { renderContextStrategyComparisonV043Html } from "./renderContextStrategyComparisonV043Html.js";
|
|
23
|
+
import { renderContextWindowScalingHtml } from "./renderContextWindowScalingHtml.js";
|
|
24
|
+
import { renderContextWindowScalingTextLines } from "./renderContextWindowScalingText.js";
|
|
25
|
+
import { renderIncrementalChangeStalenessHtml } from "./renderIncrementalChangeStalenessHtml.js";
|
|
26
|
+
import { renderRetrievalPrecisionRecallHtml } from "./renderRetrievalPrecisionRecallHtml.js";
|
|
27
|
+
import { renderRetrievalPrecisionRecallTextLines } from "./renderRetrievalPrecisionRecallText.js";
|
|
28
|
+
import { renderRetrievalQueryStrategyComparisonHtml } from "./renderRetrievalQueryStrategyComparisonHtml.js";
|
|
29
|
+
import { renderRetrievalQueryStrategyComparisonTextLines } from "./renderRetrievalQueryStrategyComparisonText.js";
|
|
30
|
+
import { renderWarmIndexReuseHtml } from "./renderWarmIndexReuseHtml.js";
|
|
31
|
+
import { renderIncrementalChangeStalenessSection, renderV043Section, renderWarmIndexReuseSection, } from "./renderSpecializedTextSections.js";
|
|
32
|
+
import { createReportSectionRegistry, } from "./reportSectionRegistry.js";
|
|
33
|
+
const WARM_INDEX_REUSE_PLUGIN_ID = "warm-index-reuse";
|
|
34
|
+
const CONTEXT_STRATEGY_COMPARISON_PLUGIN_ID = "context-strategy-comparison";
|
|
35
|
+
export const SPECIALIZED_REPORT_SECTION_DESCRIPTORS = [
|
|
36
|
+
{
|
|
37
|
+
id: "context-strategy.v043",
|
|
38
|
+
order: 10,
|
|
39
|
+
projectionKey: "contextStrategyComparisonV043",
|
|
40
|
+
appliesTo: { pluginIds: [CONTEXT_STRATEGY_COMPARISON_PLUGIN_ID] },
|
|
41
|
+
text: { title: "V0.4.3 Stage-Context Evidence", whenAbsent: "placeholder" },
|
|
42
|
+
build: ({ run }) => buildContextStrategyComparisonV043Report(run),
|
|
43
|
+
renderText: (lines, projection) => renderV043Section(lines, projection),
|
|
44
|
+
renderHtml: (projection) => renderContextStrategyComparisonV043Html(projection),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "warm-index.reuse",
|
|
48
|
+
order: 20,
|
|
49
|
+
projectionKey: "warmIndexReuse",
|
|
50
|
+
appliesTo: { pluginIds: [WARM_INDEX_REUSE_PLUGIN_ID] },
|
|
51
|
+
text: { title: "Warm Index Reuse Evidence", whenAbsent: "placeholder" },
|
|
52
|
+
build: ({ run }) => buildWarmIndexReuseReport(run),
|
|
53
|
+
renderText: (lines, projection) => renderWarmIndexReuseSection(lines, projection),
|
|
54
|
+
renderHtml: (projection) => renderWarmIndexReuseHtml(projection),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "incremental-change.staleness",
|
|
58
|
+
order: 30,
|
|
59
|
+
projectionKey: "incrementalChangeStaleness",
|
|
60
|
+
appliesTo: { pluginIds: [INCREMENTAL_CHANGE_STALENESS_PLUGIN_ID] },
|
|
61
|
+
text: { title: "Incremental-Change And Staleness Evidence", whenAbsent: "placeholder" },
|
|
62
|
+
// The V1/V2 schema-version dispatch stays owned by the existing plugin report builder.
|
|
63
|
+
build: ({ run }) => buildIncrementalChangeStalenessPluginReport(run),
|
|
64
|
+
renderText: (lines, projection) => renderIncrementalChangeStalenessSection(lines, projection),
|
|
65
|
+
renderHtml: (projection) => renderIncrementalChangeStalenessHtml(projection),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "context-window.scaling",
|
|
69
|
+
order: 40,
|
|
70
|
+
projectionKey: "contextWindowScaling",
|
|
71
|
+
appliesTo: { pluginIds: [CONTEXT_WINDOW_SCALING_PLUGIN_ID] },
|
|
72
|
+
text: { title: "Context Window Scaling", whenAbsent: "placeholder" },
|
|
73
|
+
build: ({ run }) => buildContextWindowScalingReport(run),
|
|
74
|
+
renderText: (lines, projection) => {
|
|
75
|
+
lines.push(...renderContextWindowScalingTextLines(projection));
|
|
76
|
+
},
|
|
77
|
+
renderHtml: (projection) => renderContextWindowScalingHtml(projection),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "retrieval.precision-recall",
|
|
81
|
+
order: 50,
|
|
82
|
+
projectionKey: "retrievalPrecisionRecall",
|
|
83
|
+
appliesTo: { pluginIds: [RETRIEVAL_PRECISION_RECALL_PLUGIN_ID] },
|
|
84
|
+
text: { title: "Retrieval Precision/Recall", whenAbsent: "placeholder" },
|
|
85
|
+
build: ({ run }) => buildRetrievalPrecisionRecallReport(run),
|
|
86
|
+
renderText: (lines, projection) => {
|
|
87
|
+
lines.push(...renderRetrievalPrecisionRecallTextLines(projection));
|
|
88
|
+
},
|
|
89
|
+
renderHtml: (projection) => renderRetrievalPrecisionRecallHtml(projection ?? null),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "retrieval.query-strategy-comparison",
|
|
93
|
+
order: 60,
|
|
94
|
+
projectionKey: "retrievalQueryStrategyComparison",
|
|
95
|
+
appliesTo: { pluginIds: [RETRIEVAL_QUERY_STRATEGY_COMPARISON_PLUGIN_ID] },
|
|
96
|
+
text: { title: "Retrieval Query Strategy Comparison", whenAbsent: "placeholder" },
|
|
97
|
+
build: ({ run }) => buildRetrievalQueryStrategyComparisonReport(run),
|
|
98
|
+
renderText: (lines, projection) => {
|
|
99
|
+
lines.push(...renderRetrievalQueryStrategyComparisonTextLines(projection));
|
|
100
|
+
},
|
|
101
|
+
renderHtml: (projection) => renderRetrievalQueryStrategyComparisonHtml(projection ?? null),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "retrieval.context-pack-generation",
|
|
105
|
+
order: 70,
|
|
106
|
+
projectionKey: "contextPackGeneration",
|
|
107
|
+
appliesTo: { pluginIds: [CONTEXT_PACK_GENERATION_PLUGIN_ID] },
|
|
108
|
+
text: { title: "Context Pack Generation", whenAbsent: "omit" },
|
|
109
|
+
build: ({ run, contextPacks }) => buildContextPackGenerationReport(run, contextPacks),
|
|
110
|
+
renderText: (lines, projection) => {
|
|
111
|
+
lines.push(...renderContextPackGenerationTextLines(projection));
|
|
112
|
+
},
|
|
113
|
+
renderHtml: (projection) => renderContextPackGenerationHtml(projection ?? null),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "agent-success.rate",
|
|
117
|
+
order: 80,
|
|
118
|
+
projectionKey: "agentSuccessRate",
|
|
119
|
+
appliesTo: { pluginIds: [AGENT_SUCCESS_RATE_PLUGIN_ID] },
|
|
120
|
+
// The family renderer writes its own headings.
|
|
121
|
+
text: { title: null, whenAbsent: "omit" },
|
|
122
|
+
build: ({ run }) => buildAgentSuccessRateReport(run),
|
|
123
|
+
renderText: (lines, projection) => {
|
|
124
|
+
lines.push(...renderAgentSuccessRateTextLines(projection));
|
|
125
|
+
},
|
|
126
|
+
renderHtml: (projection) => renderAgentSuccessRateHtml(projection ?? null),
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
let defaultRegistry;
|
|
130
|
+
/** The shared registry used by report generation. Assembled once; immutable afterwards. */
|
|
131
|
+
export function getSpecializedReportSectionRegistry() {
|
|
132
|
+
defaultRegistry ??= createReportSectionRegistry(SPECIALIZED_REPORT_SECTION_DESCRIPTORS);
|
|
133
|
+
return defaultRegistry;
|
|
134
|
+
}
|
|
135
|
+
/** Builds every registered specialized projection once, in registry order, using the family builders. */
|
|
136
|
+
export function buildSpecializedReportProjections(context) {
|
|
137
|
+
const projections = {};
|
|
138
|
+
for (const descriptor of getSpecializedReportSectionRegistry().descriptors) {
|
|
139
|
+
projections[descriptor.projectionKey] = descriptor.build(context);
|
|
140
|
+
}
|
|
141
|
+
return projections;
|
|
142
|
+
}
|
|
@@ -2,6 +2,7 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveWithinRoot } from "../../core/pathSafety.js";
|
|
4
4
|
import { buildPluginExperimentReport } from "./buildPluginExperimentReport.js";
|
|
5
|
+
import { collectMetricReportEvidence } from "./metricEvidence.js";
|
|
5
6
|
import { loadContextPackArtifacts } from "./loadContextPackArtifacts.js";
|
|
6
7
|
import { renderPluginExperimentReportHtml } from "./renderPluginExperimentReportHtml.js";
|
|
7
8
|
import { renderPluginExperimentReportText } from "./renderPluginExperimentReportText.js";
|
|
@@ -33,8 +34,9 @@ export async function writePluginExperimentReports(args) {
|
|
|
33
34
|
: outputPaths;
|
|
34
35
|
const writeOptions = args.exclusive ? { encoding: "utf8", flag: "wx" } : "utf8";
|
|
35
36
|
await writeFile(outputPaths.jsonPath, `${JSON.stringify({ report, outputPaths: serializedPaths }, null, 2)}\n`, writeOptions);
|
|
36
|
-
|
|
37
|
-
await writeFile(outputPaths.
|
|
37
|
+
const evidence = collectMetricReportEvidence(args.run);
|
|
38
|
+
await writeFile(outputPaths.htmlPath, renderPluginExperimentReportHtml(report, evidence), writeOptions);
|
|
39
|
+
await writeFile(outputPaths.textPath, renderPluginExperimentReportText(report, evidence), writeOptions);
|
|
38
40
|
return { report, outputPaths };
|
|
39
41
|
}
|
|
40
42
|
function readString(value) {
|
|
@@ -26,88 +26,6 @@ function genericSecretFileMode(relativePath) {
|
|
|
26
26
|
return "allow-unquoted";
|
|
27
27
|
return "quoted-only";
|
|
28
28
|
}
|
|
29
|
-
// Deterministic line-by-line comment filter. Preserves the original string
|
|
30
|
-
// length and every newline so line numbers computed from the filtered
|
|
31
|
-
// content remain identical to the original file; comment content (line or
|
|
32
|
-
// block) is replaced with spaces so it can never contribute to a match.
|
|
33
|
-
// Quote-aware: a comment marker inside a single- or double-quoted literal
|
|
34
|
-
// does not start a comment, and a backslash-escaped quote does not end one.
|
|
35
|
-
function stripCodeCommentsPreservingLength(content) {
|
|
36
|
-
const result = new Array(content.length);
|
|
37
|
-
let index = 0;
|
|
38
|
-
let inLineComment = false;
|
|
39
|
-
let inBlockComment = false;
|
|
40
|
-
let inSingleQuote = false;
|
|
41
|
-
let inDoubleQuote = false;
|
|
42
|
-
while (index < content.length) {
|
|
43
|
-
const ch = content[index];
|
|
44
|
-
const next = index + 1 < content.length ? content[index + 1] : "";
|
|
45
|
-
if (inLineComment) {
|
|
46
|
-
if (ch === "\n") {
|
|
47
|
-
inLineComment = false;
|
|
48
|
-
result[index] = "\n";
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
result[index] = " ";
|
|
52
|
-
}
|
|
53
|
-
index++;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (inBlockComment) {
|
|
57
|
-
if (ch === "*" && next === "/") {
|
|
58
|
-
result[index] = " ";
|
|
59
|
-
result[index + 1] = " ";
|
|
60
|
-
inBlockComment = false;
|
|
61
|
-
index += 2;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
result[index] = ch === "\n" ? "\n" : " ";
|
|
65
|
-
index++;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (inSingleQuote || inDoubleQuote) {
|
|
69
|
-
result[index] = ch;
|
|
70
|
-
if (ch === "\\" && next !== "") {
|
|
71
|
-
result[index + 1] = next;
|
|
72
|
-
index += 2;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if ((inSingleQuote && ch === "'") || (inDoubleQuote && ch === '"')) {
|
|
76
|
-
inSingleQuote = false;
|
|
77
|
-
inDoubleQuote = false;
|
|
78
|
-
}
|
|
79
|
-
index++;
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
if (ch === "/" && next === "/") {
|
|
83
|
-
inLineComment = true;
|
|
84
|
-
result[index] = " ";
|
|
85
|
-
index++;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (ch === "/" && next === "*") {
|
|
89
|
-
inBlockComment = true;
|
|
90
|
-
result[index] = " ";
|
|
91
|
-
index++;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (ch === "'") {
|
|
95
|
-
inSingleQuote = true;
|
|
96
|
-
result[index] = ch;
|
|
97
|
-
index++;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (ch === '"') {
|
|
101
|
-
inDoubleQuote = true;
|
|
102
|
-
result[index] = ch;
|
|
103
|
-
index++;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
result[index] = ch;
|
|
107
|
-
index++;
|
|
108
|
-
}
|
|
109
|
-
return result.join("");
|
|
110
|
-
}
|
|
111
29
|
const GENERIC_SENSITIVE_SINGLE_WORDS = new Set(["password", "passwd", "secret", "token"]);
|
|
112
30
|
function splitGenericIdentifierWords(identifier) {
|
|
113
31
|
const unquoted = identifier.replace(/^['"]|['"]$/g, "");
|
|
@@ -159,65 +77,160 @@ function isDynamicUnquotedValue(rawValue) {
|
|
|
159
77
|
const QUOTED_ASSIGNMENT_PATTERN = /(?<![A-Za-z0-9_$])(["']?)([A-Za-z_$][A-Za-z0-9_$]*)\1\s*[:=]\s*(["'])((?:\\.|(?!\3)[^\\\r\n])*)\3/g;
|
|
160
78
|
const UNQUOTED_CONFIG_PATTERN = /^([A-Za-z_][A-Za-z0-9_.-]*)\s*[:=]\s*(\S.*?)\s*$/;
|
|
161
79
|
const MIN_GENERIC_SECRET_LENGTH = 8;
|
|
162
|
-
|
|
80
|
+
// A whole-value `${...}` reference is a template/interpolation reference, not
|
|
81
|
+
// a literal credential, even when it appears inside a plain quoted string.
|
|
82
|
+
const WHOLE_VALUE_INTERPOLATION = /^\$\{[^}]*\}$/;
|
|
83
|
+
const IDENTIFIER_PART = /[A-Za-z0-9_$]/;
|
|
84
|
+
function isCandidateSecretValue(identifier, rawValue) {
|
|
85
|
+
if (!isGenericSecretIdentifier(identifier))
|
|
86
|
+
return false;
|
|
87
|
+
if (rawValue.length < MIN_GENERIC_SECRET_LENGTH)
|
|
88
|
+
return false;
|
|
89
|
+
if (WHOLE_VALUE_INTERPOLATION.test(rawValue))
|
|
90
|
+
return false;
|
|
91
|
+
if (isPlaceholder(rawValue))
|
|
92
|
+
return false;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
// Deterministic lexical-state scan for code files. Assignments are only
|
|
96
|
+
// recognized in code state, so assignment-shaped text inside an enclosing
|
|
97
|
+
// string literal, template-literal text, or a comment is never reported,
|
|
98
|
+
// while `${...}` expressions inside templates are scanned as code again.
|
|
99
|
+
// Offsets index the original content, so line numbers are exact. Single- and
|
|
100
|
+
// double-quoted strings end at an unescaped newline, as in JavaScript, so an
|
|
101
|
+
// unbalanced quote cannot swallow the rest of the file.
|
|
102
|
+
function scanQuotedCodeAssignments(relativePath, content) {
|
|
163
103
|
const findings = [];
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const rawCandidate = unquotedMatch[2];
|
|
180
|
-
if (isDynamicUnquotedValue(rawCandidate))
|
|
181
|
-
continue;
|
|
182
|
-
const rawValue = stripSurroundingQuotes(rawCandidate);
|
|
183
|
-
if (rawValue.length < MIN_GENERIC_SECRET_LENGTH)
|
|
184
|
-
continue;
|
|
185
|
-
if (isPlaceholder(rawValue))
|
|
186
|
-
continue;
|
|
187
|
-
findings.push({
|
|
188
|
-
kind: "generic-secret-assignment",
|
|
189
|
-
file: relativePath,
|
|
190
|
-
line: lineIndex + 1,
|
|
191
|
-
matchedText: `${identifier} ${line.includes(":") ? ":" : "="} [REDACTED]`,
|
|
192
|
-
matchLength: rawValue.length,
|
|
193
|
-
severity: SEVERITY_FOR_KIND["generic-secret-assignment"],
|
|
194
|
-
});
|
|
195
|
-
continue;
|
|
104
|
+
const pattern = new RegExp(QUOTED_ASSIGNMENT_PATTERN.source, "y");
|
|
105
|
+
const length = content.length;
|
|
106
|
+
const skipString = (start) => {
|
|
107
|
+
const quote = content[start];
|
|
108
|
+
let i = start + 1;
|
|
109
|
+
while (i < length) {
|
|
110
|
+
const ch = content[i];
|
|
111
|
+
if (ch === "\\")
|
|
112
|
+
i += 2;
|
|
113
|
+
else if (ch === quote)
|
|
114
|
+
return i + 1;
|
|
115
|
+
else if (ch === "\n")
|
|
116
|
+
return i;
|
|
117
|
+
else
|
|
118
|
+
i++;
|
|
196
119
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
120
|
+
return length;
|
|
121
|
+
};
|
|
122
|
+
const lexTemplate = (start) => {
|
|
123
|
+
let i = start;
|
|
124
|
+
while (i < length) {
|
|
125
|
+
const ch = content[i];
|
|
126
|
+
if (ch === "\\")
|
|
127
|
+
i += 2;
|
|
128
|
+
else if (ch === "`")
|
|
129
|
+
return i + 1;
|
|
130
|
+
else if (ch === "$" && content[i + 1] === "{")
|
|
131
|
+
i = lexCode(i + 2, true);
|
|
132
|
+
else
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
return length;
|
|
136
|
+
};
|
|
137
|
+
const lexCode = (start, stopAtCloseBrace) => {
|
|
138
|
+
let i = start;
|
|
139
|
+
let depth = 0;
|
|
140
|
+
while (i < length) {
|
|
141
|
+
const ch = content[i];
|
|
142
|
+
const next = i + 1 < length ? content[i + 1] : "";
|
|
143
|
+
if (ch === "/" && next === "/") {
|
|
144
|
+
const end = content.indexOf("\n", i);
|
|
145
|
+
i = end === -1 ? length : end;
|
|
204
146
|
continue;
|
|
205
147
|
}
|
|
206
|
-
if (
|
|
148
|
+
if (ch === "/" && next === "*") {
|
|
149
|
+
const end = content.indexOf("*/", i + 2);
|
|
150
|
+
i = end === -1 ? length : end + 2;
|
|
207
151
|
continue;
|
|
208
|
-
|
|
152
|
+
}
|
|
153
|
+
if (ch === "`") {
|
|
154
|
+
i = lexTemplate(i + 1);
|
|
209
155
|
continue;
|
|
210
|
-
|
|
156
|
+
}
|
|
157
|
+
if (stopAtCloseBrace) {
|
|
158
|
+
if (ch === "{")
|
|
159
|
+
depth++;
|
|
160
|
+
else if (ch === "}") {
|
|
161
|
+
if (depth === 0)
|
|
162
|
+
return i + 1;
|
|
163
|
+
depth--;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const isQuote = ch === '"' || ch === "'";
|
|
167
|
+
if (isQuote || /[A-Za-z_$]/.test(ch)) {
|
|
168
|
+
pattern.lastIndex = i;
|
|
169
|
+
const match = pattern.exec(content);
|
|
170
|
+
if (match && match[0].length > 0) {
|
|
171
|
+
if (isCandidateSecretValue(match[2], match[4])) {
|
|
172
|
+
findings.push({
|
|
173
|
+
kind: "generic-secret-assignment",
|
|
174
|
+
file: relativePath,
|
|
175
|
+
line: lineNumberAt(content, match.index),
|
|
176
|
+
matchedText: `${match[2]} [REDACTED]`,
|
|
177
|
+
matchLength: match[4].length,
|
|
178
|
+
severity: SEVERITY_FOR_KIND["generic-secret-assignment"],
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
i = match.index + match[0].length;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (isQuote) {
|
|
185
|
+
i = skipString(i);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
let j = i + 1;
|
|
189
|
+
while (j < length && IDENTIFIER_PART.test(content[j]))
|
|
190
|
+
j++;
|
|
191
|
+
i = j;
|
|
211
192
|
continue;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
file: relativePath,
|
|
215
|
-
line: lineIndex + 1,
|
|
216
|
-
matchedText: `${identifier} [REDACTED]`,
|
|
217
|
-
matchLength: rawValue.length,
|
|
218
|
-
severity: SEVERITY_FOR_KIND["generic-secret-assignment"],
|
|
219
|
-
});
|
|
193
|
+
}
|
|
194
|
+
i++;
|
|
220
195
|
}
|
|
196
|
+
return length;
|
|
197
|
+
};
|
|
198
|
+
lexCode(0, false);
|
|
199
|
+
return findings;
|
|
200
|
+
}
|
|
201
|
+
function scanGenericSecretAssignments(relativePath, content) {
|
|
202
|
+
const mode = genericSecretFileMode(relativePath);
|
|
203
|
+
if (mode === "quoted-only")
|
|
204
|
+
return scanQuotedCodeAssignments(relativePath, content);
|
|
205
|
+
const findings = [];
|
|
206
|
+
const lines = content.split("\n");
|
|
207
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
208
|
+
const line = lines[lineIndex];
|
|
209
|
+
const trimmed = line.trim();
|
|
210
|
+
if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.startsWith(";"))
|
|
211
|
+
continue;
|
|
212
|
+
const unquotedMatch = UNQUOTED_CONFIG_PATTERN.exec(trimmed);
|
|
213
|
+
if (!unquotedMatch)
|
|
214
|
+
continue;
|
|
215
|
+
const identifier = unquotedMatch[1];
|
|
216
|
+
if (!isGenericSecretIdentifier(identifier))
|
|
217
|
+
continue;
|
|
218
|
+
const rawCandidate = unquotedMatch[2];
|
|
219
|
+
if (isDynamicUnquotedValue(rawCandidate))
|
|
220
|
+
continue;
|
|
221
|
+
const rawValue = stripSurroundingQuotes(rawCandidate);
|
|
222
|
+
if (rawValue.length < MIN_GENERIC_SECRET_LENGTH)
|
|
223
|
+
continue;
|
|
224
|
+
if (isPlaceholder(rawValue))
|
|
225
|
+
continue;
|
|
226
|
+
findings.push({
|
|
227
|
+
kind: "generic-secret-assignment",
|
|
228
|
+
file: relativePath,
|
|
229
|
+
line: lineIndex + 1,
|
|
230
|
+
matchedText: `${identifier} ${line.includes(":") ? ":" : "="} [REDACTED]`,
|
|
231
|
+
matchLength: rawValue.length,
|
|
232
|
+
severity: SEVERITY_FOR_KIND["generic-secret-assignment"],
|
|
233
|
+
});
|
|
221
234
|
}
|
|
222
235
|
return findings;
|
|
223
236
|
}
|