redlinegate 0.0.1
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/LICENSE +21 -0
- package/README.md +188 -0
- package/commands/redline-init.md +54 -0
- package/commands/redline-verify.md +69 -0
- package/dist/bin/redline.js +605 -0
- package/dist/bin/redline.js.map +1 -0
- package/dist/commands/exempt.js +45 -0
- package/dist/commands/exempt.js.map +1 -0
- package/dist/commands/init.js +615 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/policy.js +25 -0
- package/dist/commands/policy.js.map +1 -0
- package/dist/commands/remove.js +427 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/review.js +72 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/sync.js +42 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/verify.js +478 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/config/redline-json.js +187 -0
- package/dist/config/redline-json.js.map +1 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +152 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/log.js +22 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/severity.js +16 -0
- package/dist/core/severity.js.map +1 -0
- package/dist/core/version.js +15 -0
- package/dist/core/version.js.map +1 -0
- package/dist/detect/scan.js +79 -0
- package/dist/detect/scan.js.map +1 -0
- package/dist/detect/stack.js +71 -0
- package/dist/detect/stack.js.map +1 -0
- package/dist/enforce/ladder.js +148 -0
- package/dist/enforce/ladder.js.map +1 -0
- package/dist/exempt/parse.js +86 -0
- package/dist/exempt/parse.js.map +1 -0
- package/dist/metrics/options.js +257 -0
- package/dist/metrics/options.js.map +1 -0
- package/dist/metrics/run.js +72 -0
- package/dist/metrics/run.js.map +1 -0
- package/dist/platforms/azure/client.js +53 -0
- package/dist/platforms/azure/client.js.map +1 -0
- package/dist/platforms/azure/index.js +69 -0
- package/dist/platforms/azure/index.js.map +1 -0
- package/dist/platforms/azure/install.js +917 -0
- package/dist/platforms/azure/install.js.map +1 -0
- package/dist/platforms/azure/policy-types.js +70 -0
- package/dist/platforms/azure/policy-types.js.map +1 -0
- package/dist/platforms/azure/verify.js +309 -0
- package/dist/platforms/azure/verify.js.map +1 -0
- package/dist/platforms/detect.js +40 -0
- package/dist/platforms/detect.js.map +1 -0
- package/dist/platforms/github/client.js +49 -0
- package/dist/platforms/github/client.js.map +1 -0
- package/dist/platforms/github/index.js +54 -0
- package/dist/platforms/github/index.js.map +1 -0
- package/dist/platforms/github/install.js +558 -0
- package/dist/platforms/github/install.js.map +1 -0
- package/dist/platforms/github/push.js +102 -0
- package/dist/platforms/github/push.js.map +1 -0
- package/dist/platforms/github/remote.js +36 -0
- package/dist/platforms/github/remote.js.map +1 -0
- package/dist/platforms/github/verify.js +366 -0
- package/dist/platforms/github/verify.js.map +1 -0
- package/dist/platforms/http.js +58 -0
- package/dist/platforms/http.js.map +1 -0
- package/dist/platforms/pull-request-templates.js +157 -0
- package/dist/platforms/pull-request-templates.js.map +1 -0
- package/dist/platforms/remote.js +21 -0
- package/dist/platforms/remote.js.map +1 -0
- package/dist/platforms/resolve.js +69 -0
- package/dist/platforms/resolve.js.map +1 -0
- package/dist/platforms/shape.js +19 -0
- package/dist/platforms/shape.js.map +1 -0
- package/dist/platforms/types.js +24 -0
- package/dist/platforms/types.js.map +1 -0
- package/dist/policy/checks.js +91 -0
- package/dist/policy/checks.js.map +1 -0
- package/dist/policy/diff.js +50 -0
- package/dist/policy/diff.js.map +1 -0
- package/dist/registry/discover.js +74 -0
- package/dist/registry/discover.js.map +1 -0
- package/dist/registry/serialize.js +41 -0
- package/dist/registry/serialize.js.map +1 -0
- package/dist/registry/types.js +2 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/remove/host.js +294 -0
- package/dist/remove/host.js.map +1 -0
- package/dist/render/commands.js +139 -0
- package/dist/render/commands.js.map +1 -0
- package/dist/render/manifest.js +82 -0
- package/dist/render/manifest.js.map +1 -0
- package/dist/render/markers.js +166 -0
- package/dist/render/markers.js.map +1 -0
- package/dist/render/profile.js +23 -0
- package/dist/render/profile.js.map +1 -0
- package/dist/render/standards.js +169 -0
- package/dist/render/standards.js.map +1 -0
- package/dist/render/vendors.js +199 -0
- package/dist/render/vendors.js.map +1 -0
- package/dist/review/engines/api.js +95 -0
- package/dist/review/engines/api.js.map +1 -0
- package/dist/review/engines/embedded.js +20 -0
- package/dist/review/engines/embedded.js.map +1 -0
- package/dist/review/engines/types.js +2 -0
- package/dist/review/engines/types.js.map +1 -0
- package/dist/review/glob.js +29 -0
- package/dist/review/glob.js.map +1 -0
- package/dist/review/prompt.js +45 -0
- package/dist/review/prompt.js.map +1 -0
- package/dist/review/rules.js +25 -0
- package/dist/review/rules.js.map +1 -0
- package/dist/review/schema.js +84 -0
- package/dist/review/schema.js.map +1 -0
- package/dist/review/scope.js +27 -0
- package/dist/review/scope.js.map +1 -0
- package/dist/sarif/map.js +58 -0
- package/dist/sarif/map.js.map +1 -0
- package/dist/sarif/parse.js +88 -0
- package/dist/sarif/parse.js.map +1 -0
- package/dist/sarif/types.js +2 -0
- package/dist/sarif/types.js.map +1 -0
- package/dist/sync/host.js +21 -0
- package/dist/sync/host.js.map +1 -0
- package/dist/sync/plan.js +47 -0
- package/dist/sync/plan.js.map +1 -0
- package/dist/sync/render.js +57 -0
- package/dist/sync/render.js.map +1 -0
- package/dist/sync/run.js +118 -0
- package/dist/sync/run.js.map +1 -0
- package/dist/verify/host.js +52 -0
- package/dist/verify/host.js.map +1 -0
- package/dist/verify/remote.js +146 -0
- package/dist/verify/remote.js.map +1 -0
- package/package.json +52 -0
- package/platforms/azure/gate-template.yml +105 -0
- package/rulesets/redline-org-ruleset.json +46 -0
- package/rulesets/redline-ruleset.json +49 -0
- package/scripts/assign-rule-ids.mjs +129 -0
- package/scripts/build-baseline.mjs +124 -0
- package/scripts/build-correlation.mjs +74 -0
- package/scripts/build-dashboard.mjs +584 -0
- package/scripts/build-digest.mjs +164 -0
- package/scripts/build-inbox.mjs +133 -0
- package/scripts/build-registry.mjs +42 -0
- package/scripts/build-roi.mjs +181 -0
- package/scripts/check-pins.mjs +88 -0
- package/scripts/collect-telemetry.mjs +300 -0
- package/scripts/lib/__tests__/baseline.test.mjs +132 -0
- package/scripts/lib/__tests__/correlate.test.mjs +131 -0
- package/scripts/lib/__tests__/dora.test.mjs +110 -0
- package/scripts/lib/__tests__/exemptions.test.mjs +85 -0
- package/scripts/lib/__tests__/metrics-scanner.test.mjs +65 -0
- package/scripts/lib/__tests__/sarif.test.mjs +125 -0
- package/scripts/lib/__tests__/spend.test.mjs +64 -0
- package/scripts/lib/baseline.mjs +156 -0
- package/scripts/lib/correlate.mjs +125 -0
- package/scripts/lib/dora.mjs +133 -0
- package/scripts/lib/exemptions.mjs +79 -0
- package/scripts/lib/metrics.mjs +172 -0
- package/scripts/lib/rules.mjs +107 -0
- package/scripts/lib/sarif.mjs +108 -0
- package/scripts/lib/spend.mjs +69 -0
- package/scripts/measure-context.mjs +101 -0
- package/scripts/render-self.mjs +18 -0
- package/scripts/score-seeds.mjs +294 -0
- package/scripts/validate.mjs +394 -0
- package/standards/core.md +124 -0
- package/standards/manifest.json +214 -0
- package/standards/stacks/csharp.md +29 -0
- package/standards/stacks/go.md +30 -0
- package/standards/stacks/java.md +30 -0
- package/standards/stacks/javascript.md +42 -0
- package/standards/stacks/kotlin.md +27 -0
- package/standards/stacks/microservices.md +37 -0
- package/standards/stacks/nodejs.md +34 -0
- package/standards/stacks/python.md +29 -0
- package/standards/stacks/react-native.md +29 -0
- package/standards/stacks/react.md +46 -0
- package/standards/stacks/swift.md +26 -0
- package/standards/stacks/terraform.md +27 -0
- package/templates/CODEOWNERS +29 -0
- package/templates/azure/pull_request_template.md +72 -0
- package/templates/github/pull_request_template.md +72 -0
- package/templates/redline.yml +35 -0
- package/templates/repo-context.md +64 -0
- package/workflows/dashboard.yml +119 -0
- package/workflows/inbox.yml +97 -0
- package/workflows/redline-collect.yml +61 -0
- package/workflows/redline-gate.yml +301 -0
- package/workflows/redline-sync.yml +79 -0
- package/workflows/seed-canary.yml +231 -0
- package/workflows/verify-onboarding.yml +115 -0
- package/workflows/weekly-digest.yml +78 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Redline",
|
|
3
|
+
"target": "branch",
|
|
4
|
+
"enforcement": "active",
|
|
5
|
+
"conditions": {
|
|
6
|
+
"ref_name": {
|
|
7
|
+
"include": [
|
|
8
|
+
"~DEFAULT_BRANCH"
|
|
9
|
+
],
|
|
10
|
+
"exclude": []
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"rules": [
|
|
14
|
+
{
|
|
15
|
+
"type": "deletion"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "non_fast_forward"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "pull_request",
|
|
22
|
+
"parameters": {
|
|
23
|
+
"required_approving_review_count": 1,
|
|
24
|
+
"dismiss_stale_reviews_on_push": true,
|
|
25
|
+
"require_code_owner_review": true,
|
|
26
|
+
"require_last_push_approval": true,
|
|
27
|
+
"required_review_thread_resolution": true,
|
|
28
|
+
"automatic_copilot_code_review_enabled": true,
|
|
29
|
+
"allowed_merge_methods": [
|
|
30
|
+
"squash",
|
|
31
|
+
"merge"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"type": "required_status_checks",
|
|
37
|
+
"parameters": {
|
|
38
|
+
"strict_required_status_checks_policy": false,
|
|
39
|
+
"do_not_enforce_on_create": false,
|
|
40
|
+
"required_status_checks": [
|
|
41
|
+
{
|
|
42
|
+
"context": "redline-gate / gate"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"bypass_actors": []
|
|
49
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Assigns a stable `<stack>/<slug>` id to every normative rule in standards/ that does
|
|
3
|
+
// not already have one, and rewrites the source file in place.
|
|
4
|
+
//
|
|
5
|
+
// node scripts/assign-rule-ids.mjs # assign missing ids
|
|
6
|
+
// node scripts/assign-rule-ids.mjs --check # fail if any rule is missing an id
|
|
7
|
+
//
|
|
8
|
+
// Ids are written into the source markdown, not derived at render time, because a
|
|
9
|
+
// derived id changes whenever someone rewords the rule — and then every historical
|
|
10
|
+
// telemetry record for that rule silently orphans. Once written, an id is permanent:
|
|
11
|
+
// edit the prose freely, never the id.
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join, dirname, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const CHECK = process.argv.includes('--check');
|
|
19
|
+
const manifest = JSON.parse(readFileSync(join(ROOT, 'standards/manifest.json'), 'utf8'));
|
|
20
|
+
|
|
21
|
+
// Headings whose bullets are normative rules. "What NOT to flag" is deliberately absent:
|
|
22
|
+
// those are anti-rules and are never reported as findings.
|
|
23
|
+
const RULE_HEADING = /^#{2,3}\s+(BLOCKER|HIGH|SUGGESTION|Security|Type safety|Error handling|General correctness|Scope discipline)\b/i;
|
|
24
|
+
const ANY_HEADING = /^#{1,6}\s/;
|
|
25
|
+
const HAS_ID = /^-\s+`[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*`\s+—\s/;
|
|
26
|
+
const BULLET = /^-\s+(.*)$/;
|
|
27
|
+
|
|
28
|
+
const STOP = new Set([
|
|
29
|
+
'the', 'a', 'an', 'and', 'or', 'of', 'in', 'on', 'to', 'for', 'with', 'without',
|
|
30
|
+
'that', 'this', 'is', 'are', 'be', 'no', 'not', 'new', 'code', 'inside', 'from',
|
|
31
|
+
'where', 'when', 'must', 'any', 'all', 'its', 'it', 'as', 'at', 'by',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function slugFor(text) {
|
|
35
|
+
// Prefer the bold lead-in — it is the rule's name. Fall back to the first sentence.
|
|
36
|
+
const bold = /^\*\*(.+?)\*\*/s.exec(text);
|
|
37
|
+
const source = bold ? bold[1] : text.split(/[.—:]/)[0];
|
|
38
|
+
const words = source
|
|
39
|
+
.replace(/`([^`]*)`/g, ' $1 ')
|
|
40
|
+
.replace(/[^A-Za-z0-9\s.+#-]/g, ' ')
|
|
41
|
+
.replace(/\./g, ' ')
|
|
42
|
+
.split(/\s+/)
|
|
43
|
+
.map((w) => w.toLowerCase().replace(/^-+|-+$/g, ''))
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
|
|
46
|
+
const kept = [];
|
|
47
|
+
for (const word of words) {
|
|
48
|
+
if (kept.length >= 4) break;
|
|
49
|
+
if (STOP.has(word) && kept.length) continue;
|
|
50
|
+
if (STOP.has(word) && !kept.length && words.length > 1) continue;
|
|
51
|
+
kept.push(word.replace(/\+\+/g, 'pp').replace(/#/g, 'sharp').replace(/[^a-z0-9-]/g, ''));
|
|
52
|
+
}
|
|
53
|
+
const slug = kept.filter(Boolean).join('-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
54
|
+
return slug || 'rule';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const targets = [
|
|
58
|
+
['core', 'standards/core.md'],
|
|
59
|
+
...Object.entries(manifest.stacks).map(([id, s]) => [id, s.source]),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const seen = new Map(); // id -> "file:line"
|
|
63
|
+
const missing = [];
|
|
64
|
+
let assigned = 0;
|
|
65
|
+
|
|
66
|
+
for (const [prefix, relPath] of targets) {
|
|
67
|
+
const path = join(ROOT, relPath);
|
|
68
|
+
const lines = readFileSync(path, 'utf8').split('\n');
|
|
69
|
+
const used = new Set();
|
|
70
|
+
let inRules = false;
|
|
71
|
+
let inFence = false;
|
|
72
|
+
let changed = false;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
const line = lines[i];
|
|
76
|
+
|
|
77
|
+
if (/^```/.test(line.trim())) inFence = !inFence;
|
|
78
|
+
if (inFence) continue;
|
|
79
|
+
|
|
80
|
+
if (ANY_HEADING.test(line)) {
|
|
81
|
+
inRules = RULE_HEADING.test(line);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!inRules) continue;
|
|
85
|
+
|
|
86
|
+
const bullet = BULLET.exec(line);
|
|
87
|
+
if (!bullet) continue;
|
|
88
|
+
|
|
89
|
+
if (HAS_ID.test(line)) {
|
|
90
|
+
const id = /^-\s+`([^`]+)`/.exec(line)[1];
|
|
91
|
+
if (seen.has(id)) {
|
|
92
|
+
console.error(`FAIL duplicate rule id "${id}" at ${relPath}:${i + 1} and ${seen.get(id)}`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
seen.set(id, `${relPath}:${i + 1}`);
|
|
96
|
+
used.add(id);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (CHECK) {
|
|
101
|
+
missing.push(`${relPath}:${i + 1} ${bullet[1].slice(0, 70)}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let slug = slugFor(bullet[1]);
|
|
106
|
+
let id = `${prefix}/${slug}`;
|
|
107
|
+
let n = 2;
|
|
108
|
+
while (used.has(id) || seen.has(id)) id = `${prefix}/${slug}-${n++}`;
|
|
109
|
+
used.add(id);
|
|
110
|
+
seen.set(id, `${relPath}:${i + 1}`);
|
|
111
|
+
|
|
112
|
+
lines[i] = `- \`${id}\` — ${bullet[1]}`;
|
|
113
|
+
changed = true;
|
|
114
|
+
assigned += 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (changed) writeFileSync(path, lines.join('\n'));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (CHECK) {
|
|
121
|
+
if (missing.length) {
|
|
122
|
+
console.error(`${missing.length} rule(s) missing an id:`);
|
|
123
|
+
missing.forEach((m) => console.error(` ${m}`));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
console.log(`all ${seen.size} rules have ids`);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(`assigned ${assigned} new id(s); ${seen.size} rules total`);
|
|
129
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Computes the Phase 0 baseline: the numbers every later phase in the roadmap is
|
|
3
|
+
// judged against.
|
|
4
|
+
//
|
|
5
|
+
// Run once, by the owner, with org credentials. Every figure it cannot source is
|
|
6
|
+
// reported as unavailable WITH ITS REASON rather than as zero — a zero that means
|
|
7
|
+
// "nobody measured this" reads as a finding, and makes every later comparison
|
|
8
|
+
// look like progress that did not happen.
|
|
9
|
+
//
|
|
10
|
+
// The roadmap's Phase 0 acceptance list, and where each figure comes from:
|
|
11
|
+
// repositories onboarded ......... registry.json (the derived register)
|
|
12
|
+
// today's acted-on rate .......... collected telemetry under data/
|
|
13
|
+
// findings per week .............. collected telemetry
|
|
14
|
+
// merge rate on Redline's own PRs the GitHub search API (needs ORG + token)
|
|
15
|
+
// SARIF producers in use ......... a scan of each repo's workflows (needs token)
|
|
16
|
+
// token spend per review ......... the AI vendor's own usage reporting, which
|
|
17
|
+
// this script cannot read — supply it with
|
|
18
|
+
// SPEND_TOTAL/SPEND_CURRENCY/SPEND_GRAIN
|
|
19
|
+
//
|
|
20
|
+
// Env: [DATA_DIR=data], [REGISTRY=registry.json], [DAYS=90], [OUT=baseline.json],
|
|
21
|
+
// [GH_TOKEN + ORG] to survey Redline's own PRs and SARIF producers,
|
|
22
|
+
// [SPEND_TOTAL, SPEND_CURRENCY=USD, SPEND_GRAIN=org]
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { loadRecords, aggregate } from './lib/metrics.mjs';
|
|
26
|
+
import { buildBaseline, formatBaseline } from './lib/baseline.mjs';
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
DATA_DIR = 'data',
|
|
30
|
+
REGISTRY = 'registry.json',
|
|
31
|
+
DAYS = '90',
|
|
32
|
+
OUT = 'baseline.json',
|
|
33
|
+
GH_TOKEN,
|
|
34
|
+
ORG,
|
|
35
|
+
SPEND_TOTAL,
|
|
36
|
+
SPEND_CURRENCY = 'USD',
|
|
37
|
+
SPEND_GRAIN = 'org',
|
|
38
|
+
} = process.env;
|
|
39
|
+
|
|
40
|
+
const windowDays = Number(DAYS);
|
|
41
|
+
const since = new Date(Date.now() - windowDays * 86400000).toISOString();
|
|
42
|
+
|
|
43
|
+
const { records, problems } = loadRecords(DATA_DIR, since);
|
|
44
|
+
for (const problem of problems) console.warn(` ${problem}`);
|
|
45
|
+
|
|
46
|
+
const registry = existsSync(REGISTRY)
|
|
47
|
+
? JSON.parse(readFileSync(REGISTRY, 'utf8'))
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
async function gh(path) {
|
|
51
|
+
const response = await fetch(`https://api.github.com${path}`, {
|
|
52
|
+
headers: {
|
|
53
|
+
authorization: `Bearer ${GH_TOKEN}`,
|
|
54
|
+
accept: 'application/vnd.github+json',
|
|
55
|
+
'user-agent': 'redlinegate',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) throw new Error(`GitHub ${path} returned ${response.status}`);
|
|
59
|
+
return response.json();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Redline's own pull requests across the estate — the guardrail that catches the
|
|
63
|
+
// estate quietly declining while every other number looks fine.
|
|
64
|
+
let ownPullRequests = null;
|
|
65
|
+
if (GH_TOKEN && ORG) {
|
|
66
|
+
try {
|
|
67
|
+
const query = encodeURIComponent(`org:${ORG} is:pr head:redline/ created:>${since.slice(0, 10)}`);
|
|
68
|
+
const result = await gh(`/search/issues?q=${query}&per_page=100`);
|
|
69
|
+
ownPullRequests = (result.items ?? []).map((pr) => ({ merged: Boolean(pr.pull_request?.merged_at) }));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.warn(` could not survey Redline's own pull requests: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Which SARIF producers the estate already runs — roadmap open question 1, and
|
|
76
|
+
// the input that decides whether Phase 1 is the right first move at all.
|
|
77
|
+
let sarifProducers = null;
|
|
78
|
+
if (GH_TOKEN && ORG && registry) {
|
|
79
|
+
sarifProducers = {};
|
|
80
|
+
const KNOWN = [
|
|
81
|
+
['codeql-action', 'CodeQL'],
|
|
82
|
+
['snyk', 'Snyk'],
|
|
83
|
+
['semgrep', 'Semgrep'],
|
|
84
|
+
['sonarsource', 'SonarQube'],
|
|
85
|
+
['trivy', 'Trivy'],
|
|
86
|
+
['checkmarx', 'Checkmarx'],
|
|
87
|
+
['upload-sarif', 'other (uploads SARIF)'],
|
|
88
|
+
];
|
|
89
|
+
for (const entry of registry.entries ?? []) {
|
|
90
|
+
const repo = `${entry.org}/${entry.repo}`;
|
|
91
|
+
try {
|
|
92
|
+
const listing = await gh(`/repos/${repo}/contents/.github/workflows`);
|
|
93
|
+
const tools = new Set();
|
|
94
|
+
for (const file of listing) {
|
|
95
|
+
if (!/\.ya?ml$/.test(file.name)) continue;
|
|
96
|
+
const body = await gh(`/repos/${repo}/contents/${file.path}`);
|
|
97
|
+
const text = Buffer.from(body.content ?? '', 'base64').toString('utf8').toLowerCase();
|
|
98
|
+
for (const [needle, name] of KNOWN) if (text.includes(needle)) tools.add(name);
|
|
99
|
+
}
|
|
100
|
+
if (tools.size > 0) sarifProducers[repo] = [...tools];
|
|
101
|
+
} catch {
|
|
102
|
+
// A repository whose workflows this token cannot list is not evidence that
|
|
103
|
+
// it runs no scanner. Skipping it undercounts; claiming zero would be a
|
|
104
|
+
// stronger and wronger statement.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const spend = SPEND_TOTAL
|
|
110
|
+
? { total: Number(SPEND_TOTAL), currency: SPEND_CURRENCY, grain: SPEND_GRAIN }
|
|
111
|
+
: null;
|
|
112
|
+
|
|
113
|
+
const baseline = buildBaseline({
|
|
114
|
+
aggregate: aggregate(records),
|
|
115
|
+
windowDays,
|
|
116
|
+
registry,
|
|
117
|
+
ownPullRequests,
|
|
118
|
+
sarifProducers,
|
|
119
|
+
spend,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
writeFileSync(OUT, `${JSON.stringify(baseline, null, 2)}\n`);
|
|
123
|
+
console.log(formatBaseline(baseline));
|
|
124
|
+
console.log(`\nWritten to ${OUT}.`);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Research: does ignoring a finding cost anything?
|
|
3
|
+
//
|
|
4
|
+
// Redline can say a rule was ignored. It cannot say ignoring it mattered. Where a
|
|
5
|
+
// finding was left unresolved and the same file's repository later attracted a
|
|
6
|
+
// revert or a hotfix, that is evidence the rule earns its place — from git and
|
|
7
|
+
// merged-pull-request history alone, with no incident feed, which is what keeps it
|
|
8
|
+
// inside the roadmap's non-goals.
|
|
9
|
+
//
|
|
10
|
+
// The roadmap calls this the most speculative item on the list and says to cut it
|
|
11
|
+
// without regret if the signal is too weak. So the output is a verdict on the
|
|
12
|
+
// experiment as much as on any rule: below the sample threshold it reports that it
|
|
13
|
+
// cannot say, rather than a number. A correlation quoted without its sample size
|
|
14
|
+
// is how a plausible story becomes a policy nobody can unwind.
|
|
15
|
+
//
|
|
16
|
+
// Env: [DATA_DIR=data], [DAYS=180], [WINDOW_DAYS=30], [MIN_SAMPLE=10], [OUT=correlation.json]
|
|
17
|
+
|
|
18
|
+
import { writeFileSync } from 'node:fs';
|
|
19
|
+
import { loadRecords } from './lib/metrics.mjs';
|
|
20
|
+
import { correlate, MIN_SAMPLE } from './lib/correlate.mjs';
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
DATA_DIR = 'data',
|
|
24
|
+
DAYS = '180',
|
|
25
|
+
WINDOW_DAYS = '30',
|
|
26
|
+
MIN_SAMPLE: MIN = String(MIN_SAMPLE),
|
|
27
|
+
OUT = 'correlation.json',
|
|
28
|
+
} = process.env;
|
|
29
|
+
|
|
30
|
+
const since = new Date(Date.now() - Number(DAYS) * 86400000).toISOString();
|
|
31
|
+
const { records, problems } = loadRecords(DATA_DIR, since);
|
|
32
|
+
for (const problem of problems) console.warn(` ${problem}`);
|
|
33
|
+
|
|
34
|
+
const result = correlate(records, {
|
|
35
|
+
windowDays: Number(WINDOW_DAYS),
|
|
36
|
+
minSample: Number(MIN),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
writeFileSync(OUT, `${JSON.stringify(result, null, 2)}\n`);
|
|
40
|
+
|
|
41
|
+
console.log(`Ignored-finding correlation — ${DAYS}-day history, ${result.windowDays}-day remediation window\n`);
|
|
42
|
+
|
|
43
|
+
if (!result.verdict.reportable) {
|
|
44
|
+
console.log(` NOT REPORTABLE. ${result.verdict.reason}`);
|
|
45
|
+
if (result.rules.length > 0) {
|
|
46
|
+
console.log('\n What was seen, and withheld:');
|
|
47
|
+
for (const rule of result.rules.slice(0, 10)) {
|
|
48
|
+
console.log(` ${rule.ruleId.padEnd(40)} ${rule.ignored} ignored — ${rule.reason}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
console.log(
|
|
52
|
+
'\n This is the honest output of a weak experiment, not a failure of it. The roadmap\n' +
|
|
53
|
+
' sequences this piece last and says to cut it without regret if the signal stays thin.'
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
console.log(' rule ignored followed by a revert/hotfix');
|
|
57
|
+
for (const rule of result.rules.filter((r) => r.reportable)) {
|
|
58
|
+
console.log(
|
|
59
|
+
` ${rule.ruleId.padEnd(40)} ${String(rule.ignored).padStart(7)} ${(rule.rate * 100).toFixed(0)}%`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (result.verdict.rulesWithheld > 0) {
|
|
63
|
+
console.log(`\n ${result.verdict.rulesWithheld} rule(s) withheld: too few ignored findings to mean anything.`);
|
|
64
|
+
}
|
|
65
|
+
console.log(`\n ${result.verdict.caveat}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (result.unattributable > 0) {
|
|
69
|
+
console.log(
|
|
70
|
+
`\n ${result.unattributable} ignored finding(s) sat on a revert or hotfix itself and were not attributed: ` +
|
|
71
|
+
'a remediation\'s own findings say nothing about the change it remedied.'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
console.log(`\nWritten to ${OUT}.`);
|