redlinegate 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -6
- package/dist/bin/redline.js +101 -11
- package/dist/bin/redline.js.map +1 -1
- package/dist/commands/init.js +94 -23
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/remove.js +3 -1
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/verify.js +82 -2
- package/dist/commands/verify.js.map +1 -1
- package/dist/config/redline-json.js +45 -1
- package/dist/config/redline-json.js.map +1 -1
- package/dist/detect/existing.js +151 -0
- package/dist/detect/existing.js.map +1 -0
- package/dist/exempt/parse.js +8 -1
- package/dist/exempt/parse.js.map +1 -1
- package/dist/platforms/azure/install.js +8 -4
- package/dist/platforms/azure/install.js.map +1 -1
- package/dist/platforms/azure/verify.js +6 -0
- package/dist/platforms/azure/verify.js.map +1 -1
- package/dist/platforms/github/install.js +88 -10
- package/dist/platforms/github/install.js.map +1 -1
- package/dist/platforms/github/preflight.js +63 -0
- package/dist/platforms/github/preflight.js.map +1 -0
- package/dist/platforms/github/verify.js +62 -5
- package/dist/platforms/github/verify.js.map +1 -1
- package/dist/platforms/types.js +4 -0
- package/dist/platforms/types.js.map +1 -1
- package/dist/policy/diff.js +26 -9
- package/dist/policy/diff.js.map +1 -1
- package/dist/render/contexts.js +39 -0
- package/dist/render/contexts.js.map +1 -0
- package/dist/render/profile.js +44 -10
- package/dist/render/profile.js.map +1 -1
- package/dist/render/standards.js +2 -0
- package/dist/render/standards.js.map +1 -1
- package/dist/render/vendors.js +5 -1
- package/dist/render/vendors.js.map +1 -1
- package/dist/ui/facts.js +82 -0
- package/dist/ui/facts.js.map +1 -0
- package/dist/ui/prompt.js +201 -0
- package/dist/ui/prompt.js.map +1 -0
- package/dist/ui/tty.js +243 -0
- package/dist/ui/tty.js.map +1 -0
- package/dist/ui/wizard.js +179 -0
- package/dist/ui/wizard.js.map +1 -0
- package/package.json +10 -1
- package/platforms/azure/gate-template-github.yml +152 -0
- package/platforms/azure/gate-template.yml +59 -6
- package/scripts/check-pins.mjs +84 -1
- package/standards/contexts/speckit.md +22 -0
- package/standards/contexts/tmf.md +25 -0
- package/standards/manifest.json +1 -1
- package/templates/redline.yml +5 -9
- package/workflows/dashboard.yml +1 -1
- package/workflows/redline-collect.yml +1 -1
- package/workflows/redline-gate.yml +10 -3
- package/workflows/seed-canary.yml +2 -2
- package/workflows/weekly-digest.yml +1 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { RUNGS } from "../enforce/ladder.js";
|
|
2
|
+
// Plain-English, at the moment of choosing. Every one of these was a term the
|
|
3
|
+
// operator met for the first time in output that assumed they knew it.
|
|
4
|
+
const RUNG_HINTS = {
|
|
5
|
+
observe: 'comments only — the check is always green. Start here',
|
|
6
|
+
warn: 'comments and labels the PR — still never blocks a merge',
|
|
7
|
+
'block-blocker': 'a BLOCKER finding fails the check and stops the merge',
|
|
8
|
+
'block-high': 'BLOCKER and HIGH both stop the merge — the strictest rung',
|
|
9
|
+
};
|
|
10
|
+
const CAPABILITY_HINTS = {
|
|
11
|
+
gate: 'the pull request check that runs the review',
|
|
12
|
+
'merge-policy': 'branch protection requiring the check and an approval',
|
|
13
|
+
labels: 'the labels the gate uses to mark and exempt a pull request',
|
|
14
|
+
'review-ownership': 'seeds CODEOWNERS — off unless you ask, it can block every PR',
|
|
15
|
+
};
|
|
16
|
+
// Which Redline check a tool the repository already runs makes redundant.
|
|
17
|
+
// Phrased as what the operator would otherwise get twice, because "stands down
|
|
18
|
+
// the dependencies job" means nothing to someone meeting Redline today.
|
|
19
|
+
const STAND_DOWN_LABEL = {
|
|
20
|
+
secrets: 'secret scanning',
|
|
21
|
+
dependencies: 'dependency vulnerability review',
|
|
22
|
+
policy: 'static analysis',
|
|
23
|
+
};
|
|
24
|
+
function profileChoices(manifest, detected) {
|
|
25
|
+
const ids = Object.keys(manifest.profiles).sort((a, b) => {
|
|
26
|
+
if (a === detected)
|
|
27
|
+
return -1;
|
|
28
|
+
if (b === detected)
|
|
29
|
+
return 1;
|
|
30
|
+
return a.localeCompare(b);
|
|
31
|
+
});
|
|
32
|
+
return ids.map((id) => {
|
|
33
|
+
// The stacks' own titles, not their ids. `JavaScript, React (web)` tells an
|
|
34
|
+
// operator what they are choosing; `javascript, react` makes them guess at
|
|
35
|
+
// the difference between two profiles that share a prefix.
|
|
36
|
+
const stacks = (manifest.profiles[id] ?? []).map((stack) => manifest.stacks[stack]?.title ?? stack);
|
|
37
|
+
return {
|
|
38
|
+
value: id,
|
|
39
|
+
label: id,
|
|
40
|
+
hint: stacks.join(', ') + (id === detected ? ' (detected)' : ''),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Every vendor the standards know about, including the ones this organisation
|
|
45
|
+
// has switched off — shown greyed with the reason rather than filtered out.
|
|
46
|
+
//
|
|
47
|
+
// Hiding them was wrong for the same reason hiding a covered capability would
|
|
48
|
+
// be: an operator looking for Cursor and not finding it cannot tell whether
|
|
49
|
+
// Redline forgot about Cursor, does not support it, or was told not to render
|
|
50
|
+
// for it here. Only the last is true, and only the last is something they can
|
|
51
|
+
// go and change.
|
|
52
|
+
function vendorChoices(manifest) {
|
|
53
|
+
return Object.entries(manifest.vendors).map(([id, vendor]) => ({
|
|
54
|
+
value: id,
|
|
55
|
+
label: id,
|
|
56
|
+
...(vendor.enabled
|
|
57
|
+
? { hint: vendor.title }
|
|
58
|
+
: // Reason first: a long vendor title truncates, and the half worth
|
|
59
|
+
// keeping is the part that says why the row cannot be picked.
|
|
60
|
+
{ disabled: `not enabled for this organisation — ${vendor.title}` }),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Run the menu.
|
|
65
|
+
*
|
|
66
|
+
* Every question is skippable in the sense that it has a preselected answer;
|
|
67
|
+
* none is skippable in the sense of being hidden. A capability the repository
|
|
68
|
+
* already covers is still shown — deselected, with the tool that covers it
|
|
69
|
+
* named — because an operator who cannot see a decision cannot overrule it,
|
|
70
|
+
* and this repository's detection is wrong often enough that overruling has to
|
|
71
|
+
* stay one keystroke away.
|
|
72
|
+
*/
|
|
73
|
+
export async function runWizard(p, facts) {
|
|
74
|
+
const { manifest, survey, recorded } = facts;
|
|
75
|
+
p.intro('Redline');
|
|
76
|
+
// Multi-select, because a repository is routinely more than one bundle: a
|
|
77
|
+
// React application with its own Terraform beside it is `web,infra`, and a
|
|
78
|
+
// single-choice question made it pick the half that fitted worst.
|
|
79
|
+
//
|
|
80
|
+
// `.redline.json` still records one string — resolveProfile takes the list
|
|
81
|
+
// and sorts it — so nothing downstream had to learn a new shape.
|
|
82
|
+
const recordedProfiles = recorded?.profile?.split(',').map((s) => s.trim());
|
|
83
|
+
const profiles = await p.multiselect('Which standards apply here?', profileChoices(manifest, facts.detectedProfile), recordedProfiles ?? [facts.detectedProfile]);
|
|
84
|
+
// Deselecting everything renders no rules at all, which is never what the
|
|
85
|
+
// operator meant by "none of these" — they meant they could not find theirs.
|
|
86
|
+
// Falling back to what was detected keeps the run useful and keeps the answer
|
|
87
|
+
// visible in the summary, where it can be changed.
|
|
88
|
+
const profile = profiles.length > 0 ? profiles.join(',') : facts.detectedProfile;
|
|
89
|
+
const hostChoices = [
|
|
90
|
+
{
|
|
91
|
+
value: 'github',
|
|
92
|
+
label: 'GitHub',
|
|
93
|
+
hint: 'GitHub.com or GitHub Enterprise Server' + (facts.detectedHost === 'github' ? ' (detected)' : ''),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
value: 'azure',
|
|
97
|
+
label: 'Azure DevOps',
|
|
98
|
+
// Deliberately no hostnames: cli/platforms/__tests__/boundary.test.ts
|
|
99
|
+
// holds the line that only an adapter knows what a host's URLs look like,
|
|
100
|
+
// and a menu hint is not an adapter.
|
|
101
|
+
hint: 'Azure Repos' + (facts.detectedHost === 'azure' ? ' (detected)' : ''),
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
const host = await p.select('Where does this repository live?', hostChoices, facts.detectedHost ?? 'github');
|
|
105
|
+
// Asked separately from the host because the two genuinely come apart: every
|
|
106
|
+
// VFUK repository is on github.com and built by Azure Pipelines. Deriving the
|
|
107
|
+
// pipeline from the host is what put a GitHub Actions caller into a repo
|
|
108
|
+
// whose real gate is `cicd/pre-merge.yaml`.
|
|
109
|
+
const pipelineChoices = [
|
|
110
|
+
{
|
|
111
|
+
value: 'github-actions',
|
|
112
|
+
label: 'GitHub Actions',
|
|
113
|
+
hint: 'a workflow under .github/workflows',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
value: 'azure-pipelines',
|
|
117
|
+
label: 'Azure Pipelines',
|
|
118
|
+
hint: facts.existingPipeline === null
|
|
119
|
+
? 'a stage in an Azure pipeline definition'
|
|
120
|
+
: `a stage added to ${facts.existingPipeline} (detected)`,
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
const pipeline = await p.select('What runs your pull request checks?', pipelineChoices, facts.existingPipeline !== null ? 'azure-pipelines' : 'github-actions');
|
|
124
|
+
const vendors = await p.multiselect('Which assistants should read the standards?', vendorChoices(manifest), [...(recorded?.vendors ?? facts.detectedVendors)]);
|
|
125
|
+
const contexts = await p.multiselect('Extra context to render beside the rules', [
|
|
126
|
+
{
|
|
127
|
+
value: 'speckit',
|
|
128
|
+
label: 'Spec-driven development',
|
|
129
|
+
hint: 'how to work from a spec — dropped automatically if you already run Spec Kit',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
value: 'tmf',
|
|
133
|
+
label: 'TM Forum',
|
|
134
|
+
hint: 'resource naming, @type, paging, error bodies. Only if you implement TMF APIs',
|
|
135
|
+
},
|
|
136
|
+
], ['speckit']);
|
|
137
|
+
// A capability a detected tool already covers starts deselected and says
|
|
138
|
+
// which tool. `standDown` is advisory: nothing here is decided for the
|
|
139
|
+
// operator, only defaulted.
|
|
140
|
+
const covered = new Map();
|
|
141
|
+
for (const tool of survey.tools) {
|
|
142
|
+
if (tool.standsDown === null)
|
|
143
|
+
continue;
|
|
144
|
+
covered.set(tool.standsDown, tool.label);
|
|
145
|
+
}
|
|
146
|
+
const capabilityChoices = Object.entries(CAPABILITY_HINTS).map(([id, hint]) => ({ value: id, label: id, hint }));
|
|
147
|
+
const capabilities = await p.multiselect('What should Redline install?', capabilityChoices, ['gate', 'merge-policy', 'labels']);
|
|
148
|
+
if (covered.size > 0) {
|
|
149
|
+
const named = [...covered]
|
|
150
|
+
.map(([job, label]) => `${label} already covers ${STAND_DOWN_LABEL[job] ?? job}`)
|
|
151
|
+
.join('; ');
|
|
152
|
+
p.note(`${named}. Redline adds its own on top rather than replacing them.`);
|
|
153
|
+
}
|
|
154
|
+
const rung = await p.select('How hard should the check bite?', RUNGS.map((value) => ({ value, label: value, hint: RUNG_HINTS[value] })), recorded?.rung ?? 'observe');
|
|
155
|
+
const action = await p.select('Ready?', [
|
|
156
|
+
{
|
|
157
|
+
value: 'dry-run',
|
|
158
|
+
label: 'Dry run',
|
|
159
|
+
hint: 'print the plan — writes nothing, contacts no host, needs no credential',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
value: 'apply',
|
|
163
|
+
label: 'Apply',
|
|
164
|
+
hint: 'write the files and open a pull request on redline/onboard',
|
|
165
|
+
},
|
|
166
|
+
], 'dry-run');
|
|
167
|
+
return {
|
|
168
|
+
profile,
|
|
169
|
+
vendors,
|
|
170
|
+
host,
|
|
171
|
+
pipeline,
|
|
172
|
+
speckit: contexts.includes('speckit'),
|
|
173
|
+
tmf: contexts.includes('tmf'),
|
|
174
|
+
capabilities,
|
|
175
|
+
rung,
|
|
176
|
+
action,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=wizard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wizard.js","sourceRoot":"","sources":["../../cli/ui/wizard.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redlinegate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Engineering control plane for AI-assisted development: standards, merge gates and evidence across GitHub and Azure DevOps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://redline-gate.vercel.app",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/moelzanaty3/redline.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/moelzanaty3/redline/issues"
|
|
14
|
+
},
|
|
15
|
+
"author": "Mohamed Elzanaty (https://github.com/moelzanaty3)",
|
|
8
16
|
"keywords": [
|
|
9
17
|
"code-review",
|
|
10
18
|
"ai-code-review",
|
|
@@ -28,6 +36,7 @@
|
|
|
28
36
|
"redlinegate": "dist/bin/redline.js"
|
|
29
37
|
},
|
|
30
38
|
"files": [
|
|
39
|
+
"README.md",
|
|
31
40
|
"LICENSE",
|
|
32
41
|
"dist/",
|
|
33
42
|
"scripts/",
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Managed by Redline; regenerate with `redline init` rather than editing here.
|
|
2
|
+
# Redline merge gate for a GitHub-hosted repository built by Azure Pipelines.
|
|
3
|
+
#
|
|
4
|
+
# This is the combination the estate actually runs: the code lives on GitHub,
|
|
5
|
+
# the pull request checks are Azure Pipelines. Neither of Redline's two original
|
|
6
|
+
# paths fitted it — the GitHub path writes an Actions workflow the repository
|
|
7
|
+
# never runs, and the Azure path installs a Build Validation policy against an
|
|
8
|
+
# Azure Repos repository that does not exist here.
|
|
9
|
+
#
|
|
10
|
+
# CONTRACT: this pipeline reports to GitHub as a check named after its own
|
|
11
|
+
# pipeline definition, published by the Azure Pipelines GitHub App. There is no
|
|
12
|
+
# status POST here, unlike the Azure Repos template beside it: on a GitHub-hosted
|
|
13
|
+
# repository the build result IS the check, and posting to the Azure Repos
|
|
14
|
+
# statuses API would address a pull request that does not exist. The name a
|
|
15
|
+
# branch ruleset must require is therefore the pipeline's name in Azure DevOps —
|
|
16
|
+
# `redline init` prints it, and `redline verify` reads the reported names back.
|
|
17
|
+
#
|
|
18
|
+
# A `pr:` block IS honoured here. Azure Pipelines ignores `pr:` triggers only
|
|
19
|
+
# for Azure *Repos*; for a GitHub-hosted repository they are the trigger
|
|
20
|
+
# mechanism, which is why the Azure Repos template beside this one has none.
|
|
21
|
+
|
|
22
|
+
pr:
|
|
23
|
+
branches:
|
|
24
|
+
include:
|
|
25
|
+
- main
|
|
26
|
+
- master
|
|
27
|
+
|
|
28
|
+
trigger: none
|
|
29
|
+
|
|
30
|
+
pool:
|
|
31
|
+
vmImage: ubuntu-latest
|
|
32
|
+
|
|
33
|
+
variables:
|
|
34
|
+
ADR_DIFF_THRESHOLD: 300
|
|
35
|
+
FAIL_ON_DEPENDENCY_SEVERITY: high
|
|
36
|
+
SOFT_FAIL_LABELS: redline-exempt,redline-sync
|
|
37
|
+
# Declared with a default so the final step can read it even when the scan
|
|
38
|
+
# passed and never set it. An undeclared Azure variable interpolates as the
|
|
39
|
+
# literal string "$(NAME)" rather than empty, which is the kind of quiet
|
|
40
|
+
# wrong answer this gate exists to prevent.
|
|
41
|
+
REDLINE_SECURITY_FAILED: 'false'
|
|
42
|
+
# Pinned by image digest, not by tag: a tag is mutable and this scans the diff
|
|
43
|
+
# of every pull request in the repository. The version in the trailing comment
|
|
44
|
+
# is what scripts/check-pins.mjs re-resolves the digest against, exactly as it
|
|
45
|
+
# does for the SHA-pinned actions on the GitHub side of the gate.
|
|
46
|
+
TRUFFLEHOG_IMAGE: trufflesecurity/trufflehog@sha256:deb2af10659a488a14d262a323addcde099d99827a1cf1dc4e93c17915c39f08 # 3.97.1
|
|
47
|
+
|
|
48
|
+
steps:
|
|
49
|
+
- checkout: self
|
|
50
|
+
fetchDepth: 0
|
|
51
|
+
|
|
52
|
+
- task: NodeTool@0
|
|
53
|
+
displayName: Node 22
|
|
54
|
+
inputs:
|
|
55
|
+
versionSpec: '22.x'
|
|
56
|
+
|
|
57
|
+
# No substitute in Mend or SonarQube: both scan a state of the tree, and a
|
|
58
|
+
# credential committed and reverted inside one pull request is still in the
|
|
59
|
+
# history the moment it merges. This scans the RANGE.
|
|
60
|
+
#
|
|
61
|
+
# `--results=verified` only: `unknown` produces false positives and this
|
|
62
|
+
# check cannot be waived by a label.
|
|
63
|
+
- script: |
|
|
64
|
+
set -euo pipefail
|
|
65
|
+
target="${SYSTEM_PULLREQUEST_TARGETBRANCH#refs/heads/}"
|
|
66
|
+
# The PR build checks out a merge commit, and the target branch is not
|
|
67
|
+
# guaranteed to be a local ref on a fresh agent. Fetch it by name before
|
|
68
|
+
# asking for a merge base, or `git merge-base` fails and the scan silently
|
|
69
|
+
# degrades to scanning nothing.
|
|
70
|
+
git fetch --no-tags origin "$target"
|
|
71
|
+
base=$(git merge-base FETCH_HEAD HEAD)
|
|
72
|
+
echo "scanning $base..HEAD"
|
|
73
|
+
|
|
74
|
+
# Recorded BEFORE the exit, because the exemption branch in the final step
|
|
75
|
+
# must never downgrade this one.
|
|
76
|
+
if ! docker run --rm -v "$(pwd):/repo" "$TRUFFLEHOG_IMAGE" \
|
|
77
|
+
git file:///repo --since-commit "$base" --results=verified --fail
|
|
78
|
+
then
|
|
79
|
+
echo "##vso[task.setvariable variable=REDLINE_SECURITY_FAILED]true"
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
displayName: Secret scan (diff)
|
|
83
|
+
name: secrets
|
|
84
|
+
env:
|
|
85
|
+
TRUFFLEHOG_IMAGE: $(TRUFFLEHOG_IMAGE)
|
|
86
|
+
|
|
87
|
+
# --package pins npx to the redlinegate package explicitly. `redline` alone
|
|
88
|
+
# names a different, unrelated package on the public registry.
|
|
89
|
+
- script: npx --yes --package=redlinegate@latest redline verify --gate
|
|
90
|
+
displayName: Redline gate
|
|
91
|
+
name: gate
|
|
92
|
+
# A failed secret scan must not hide the configuration findings: the author
|
|
93
|
+
# deserves the whole list in one run rather than one blocker at a time.
|
|
94
|
+
condition: succeededOrFailed()
|
|
95
|
+
env:
|
|
96
|
+
GH_TOKEN: $(GH_TOKEN)
|
|
97
|
+
ADR_DIFF_THRESHOLD: $(ADR_DIFF_THRESHOLD)
|
|
98
|
+
FAIL_ON_DEPENDENCY_SEVERITY: $(FAIL_ON_DEPENDENCY_SEVERITY)
|
|
99
|
+
SOFT_FAIL_LABELS: $(SOFT_FAIL_LABELS)
|
|
100
|
+
|
|
101
|
+
- script: |
|
|
102
|
+
set -euo pipefail
|
|
103
|
+
if [ "$AGENT_JOBSTATUS" = "Succeeded" ]; then
|
|
104
|
+
echo "gate passed"
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# The secret scan is never waivable, exactly as on the GitHub side where
|
|
109
|
+
# the security jobs sit outside label exemption. A label that could waive
|
|
110
|
+
# a verified credential in the diff would make the label the
|
|
111
|
+
# vulnerability.
|
|
112
|
+
if [ "${REDLINE_SECURITY_FAILED:-}" = "true" ]; then
|
|
113
|
+
echo "##vso[task.logissue type=error]The secret scan failed. This check cannot be waived by a label."
|
|
114
|
+
exit 1
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# Soft-fail escape hatch: a pull request carrying one of SOFT_FAIL_LABELS
|
|
118
|
+
# reports success instead of blocking, so a reviewer can accept a process
|
|
119
|
+
# failure deliberately. It can only ever turn a failure into a success.
|
|
120
|
+
#
|
|
121
|
+
# Fails CLOSED when no token is configured. Without GH_TOKEN the labels
|
|
122
|
+
# cannot be read, and "could not check for an exemption" must block rather
|
|
123
|
+
# than wave through — an unreadable exemption is not an exemption.
|
|
124
|
+
if [ -z "${GH_TOKEN:-}" ] || [ -z "${SOFT_FAIL_LABELS:-}" ]; then
|
|
125
|
+
echo "##vso[task.logissue type=error]Redline gate failed."
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
pr="${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER:-}"
|
|
130
|
+
repo="${BUILD_REPOSITORY_NAME:-}"
|
|
131
|
+
# The token must never appear in argv — visible via `ps` to any process on
|
|
132
|
+
# the agent. It is already an env var; hand it to curl through a stdin
|
|
133
|
+
# config instead of putting it on the command line with -H.
|
|
134
|
+
labels=$(printf 'header = "Authorization: Bearer %s"\n' "$GH_TOKEN" \
|
|
135
|
+
| curl -K - -sS -H "Accept: application/vnd.github+json" \
|
|
136
|
+
"https://api.github.com/repos/${repo}/issues/${pr}/labels") || labels=''
|
|
137
|
+
|
|
138
|
+
for label in ${SOFT_FAIL_LABELS//,/ }; do
|
|
139
|
+
if printf '%s' "$labels" | jq -e --arg l "$label" '[.[]?.name] | index($l)' >/dev/null 2>&1; then
|
|
140
|
+
echo "##vso[task.logissue type=warning]Redline gate failed but the \"$label\" label is applied — a reviewer accepted this deliberately"
|
|
141
|
+
exit 0
|
|
142
|
+
fi
|
|
143
|
+
done
|
|
144
|
+
|
|
145
|
+
echo "##vso[task.logissue type=error]Redline gate failed."
|
|
146
|
+
exit 1
|
|
147
|
+
displayName: Redline gate verdict
|
|
148
|
+
condition: always()
|
|
149
|
+
env:
|
|
150
|
+
GH_TOKEN: $(GH_TOKEN)
|
|
151
|
+
SOFT_FAIL_LABELS: $(SOFT_FAIL_LABELS)
|
|
152
|
+
REDLINE_SECURITY_FAILED: $(REDLINE_SECURITY_FAILED)
|
|
@@ -22,6 +22,18 @@ variables:
|
|
|
22
22
|
ADR_DIFF_THRESHOLD: 300
|
|
23
23
|
FAIL_ON_DEPENDENCY_SEVERITY: high
|
|
24
24
|
SOFT_FAIL_LABELS: redline-exempt,redline-sync
|
|
25
|
+
# Declared with a default so the status step can read it even when the scan
|
|
26
|
+
# passed and never set it. An undeclared Azure variable interpolates as the
|
|
27
|
+
# literal string "$(NAME)" rather than empty, which is the kind of quiet
|
|
28
|
+
# wrong answer this gate exists to prevent.
|
|
29
|
+
REDLINE_SECURITY_FAILED: 'false'
|
|
30
|
+
# Pinned by image digest, not by tag: a tag is mutable and this scans the diff
|
|
31
|
+
# of every pull request in the repository. The version in the trailing comment
|
|
32
|
+
# is what scripts/check-pins.mjs re-resolves the digest against, exactly as it
|
|
33
|
+
# does for the SHA-pinned actions on the GitHub side of the gate. Docker Hub
|
|
34
|
+
# spells the tag without the `v` the git tag carries; both sides of the gate
|
|
35
|
+
# deliberately run the same version.
|
|
36
|
+
TRUFFLEHOG_IMAGE: trufflesecurity/trufflehog@sha256:deb2af10659a488a14d262a323addcde099d99827a1cf1dc4e93c17915c39f08 # 3.97.1
|
|
25
37
|
|
|
26
38
|
steps:
|
|
27
39
|
- checkout: self
|
|
@@ -32,12 +44,48 @@ steps:
|
|
|
32
44
|
inputs:
|
|
33
45
|
versionSpec: '22.x'
|
|
34
46
|
|
|
47
|
+
# The GitHub gate runs this as its own job (workflows/redline-gate.yml,
|
|
48
|
+
# `secrets`). It has no substitute in Mend or SonarQube: both scan a state of
|
|
49
|
+
# the tree, and a credential committed and reverted inside one pull request
|
|
50
|
+
# is still in the history the moment it merges. This scans the RANGE.
|
|
51
|
+
#
|
|
52
|
+
# `--results=verified` only, matching the GitHub job for the same reason:
|
|
53
|
+
# `unknown` produces false positives and this check cannot be waived.
|
|
54
|
+
- script: |
|
|
55
|
+
set -euo pipefail
|
|
56
|
+
target="${SYSTEM_PULLREQUEST_TARGETBRANCH#refs/heads/}"
|
|
57
|
+
# The PR build checks out a merge commit, and the target branch is not
|
|
58
|
+
# guaranteed to be a local ref on a fresh agent. Fetch it by name before
|
|
59
|
+
# asking for a merge base, or `git merge-base` fails and the scan silently
|
|
60
|
+
# degrades to scanning nothing.
|
|
61
|
+
git fetch --no-tags origin "$target"
|
|
62
|
+
base=$(git merge-base FETCH_HEAD HEAD)
|
|
63
|
+
echo "scanning $base..HEAD"
|
|
64
|
+
|
|
65
|
+
# The failure is recorded BEFORE the exit, because the exemption branch in
|
|
66
|
+
# the status step below must never downgrade this one. On GitHub the
|
|
67
|
+
# security jobs sit outside label exemption; this variable is how that
|
|
68
|
+
# exclusion survives the two being one pipeline here.
|
|
69
|
+
if ! docker run --rm -v "$(pwd):/repo" "$TRUFFLEHOG_IMAGE" \
|
|
70
|
+
git file:///repo --since-commit "$base" --results=verified --fail
|
|
71
|
+
then
|
|
72
|
+
echo "##vso[task.setvariable variable=REDLINE_SECURITY_FAILED]true"
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
displayName: Secret scan (diff)
|
|
76
|
+
name: secrets
|
|
77
|
+
env:
|
|
78
|
+
TRUFFLEHOG_IMAGE: $(TRUFFLEHOG_IMAGE)
|
|
79
|
+
|
|
35
80
|
# --package pins npx to the redlinegate package explicitly. `redline` alone
|
|
36
81
|
# names a different, unrelated package on the public registry — `redline`
|
|
37
82
|
# is only this package's bin name, never resolve npx against it directly.
|
|
38
83
|
- script: npx --yes --package=redlinegate@latest redline verify --gate
|
|
39
84
|
displayName: Redline gate
|
|
40
85
|
name: gate
|
|
86
|
+
# A failed secret scan must not hide the configuration findings: the author
|
|
87
|
+
# deserves the whole list in one run rather than one blocker at a time.
|
|
88
|
+
condition: succeededOrFailed()
|
|
41
89
|
env:
|
|
42
90
|
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
|
43
91
|
ADR_DIFF_THRESHOLD: $(ADR_DIFF_THRESHOLD)
|
|
@@ -72,12 +120,16 @@ steps:
|
|
|
72
120
|
# accept a process failure deliberately. It can only ever turn a
|
|
73
121
|
# failure into a success, never the reverse.
|
|
74
122
|
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
123
|
+
# The secret scan is excluded, exactly as it is on GitHub, where the
|
|
124
|
+
# security jobs sit outside the exemption. There the exclusion is
|
|
125
|
+
# structural — separate jobs, and the label only reaches some of them.
|
|
126
|
+
# Here the whole gate is one pipeline, so the scan records
|
|
127
|
+
# REDLINE_SECURITY_FAILED before it exits and this branch refuses to
|
|
128
|
+
# downgrade it. A label that could waive a verified credential in the
|
|
129
|
+
# diff would make the label the vulnerability.
|
|
130
|
+
if [ "${REDLINE_SECURITY_FAILED:-}" = "true" ]; then
|
|
131
|
+
echo "##vso[task.logissue type=error]The secret scan failed. This check cannot be waived by a label."
|
|
132
|
+
elif [ "$state" = "failed" ] && [ -n "${SOFT_FAIL_LABELS:-}" ]; then
|
|
81
133
|
labels=$(curl_authed -sS "$pr_url/labels?api-version=7.1") || labels=''
|
|
82
134
|
for label in ${SOFT_FAIL_LABELS//,/ }; do
|
|
83
135
|
if printf '%s' "$labels" \
|
|
@@ -103,3 +155,4 @@ steps:
|
|
|
103
155
|
env:
|
|
104
156
|
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
|
105
157
|
SOFT_FAIL_LABELS: $(SOFT_FAIL_LABELS)
|
|
158
|
+
REDLINE_SECURITY_FAILED: $(REDLINE_SECURITY_FAILED)
|
package/scripts/check-pins.mjs
CHANGED
|
@@ -39,6 +39,14 @@ const files = [
|
|
|
39
39
|
? readdirSync(join(ROOT, '.github/workflows')).map((f) => `.github/workflows/${f}`)
|
|
40
40
|
: []),
|
|
41
41
|
'templates/redline.yml',
|
|
42
|
+
// The Azure gate. It was outside this list while it pinned nothing, and the
|
|
43
|
+
// moment it pinned a container digest that omission would have made the pin
|
|
44
|
+
// unverifiable — the exact rot this script exists to catch.
|
|
45
|
+
'platforms/azure/gate-template.yml',
|
|
46
|
+
// The same gate for a GitHub-hosted repository built by Azure Pipelines. It
|
|
47
|
+
// carries the same container digest, so leaving it out here would let the two
|
|
48
|
+
// halves of the same gate drift to different versions of the scanner.
|
|
49
|
+
'platforms/azure/gate-template-github.yml',
|
|
42
50
|
].filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
43
51
|
|
|
44
52
|
const pins = [];
|
|
@@ -84,5 +92,80 @@ for (const pin of pins) {
|
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
// The Azure gate runs its secret scan from a container rather than an action,
|
|
96
|
+
// so the pin is an image digest with the version in a trailing comment — the
|
|
97
|
+
// same shape as `uses: repo@sha # tag`, verified the same way. A digest that no
|
|
98
|
+
// longer matches the tag it claims means someone edited one and not the other,
|
|
99
|
+
// and the scan of every Azure pull request is then running an unreviewed image.
|
|
100
|
+
const IMAGE_PIN = /([\w.-]+\/[\w.-]+)@(sha256:[0-9a-f]{64})\s*#\s*(\S+)/g;
|
|
101
|
+
const imagePins = [];
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const body = readFileSync(join(ROOT, file), 'utf8');
|
|
104
|
+
for (const [, image, digest, tag] of body.matchAll(IMAGE_PIN)) {
|
|
105
|
+
imagePins.push({ file, image, digest, tag });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const pin of imagePins) {
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(`https://hub.docker.com/v2/repositories/${pin.image}/tags/${pin.tag}`);
|
|
112
|
+
if (!res.ok) throw new Error(`docker hub: ${res.status}`);
|
|
113
|
+
const resolved = (await res.json()).digest;
|
|
114
|
+
if (resolved !== pin.digest) {
|
|
115
|
+
console.error(
|
|
116
|
+
`FAIL ${pin.file}: ${pin.image}@${pin.digest} is commented as ${pin.tag}, but ${pin.tag} resolves to ${resolved}`
|
|
117
|
+
);
|
|
118
|
+
errors += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
console.log(`ok ${pin.image}:${pin.tag} → ${pin.digest.slice(7, 19)}`);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(`FAIL ${pin.file}: could not verify ${pin.image}:${pin.tag} — ${err.message}`);
|
|
124
|
+
errors += 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The gate also pins the CLI it shells out to, as `REDLINE_CLI_VERSION` rather
|
|
129
|
+
// than as a `uses:` SHA, so the loop above cannot see it. It is the pin that
|
|
130
|
+
// rots hardest: it is a plain literal in two places, nothing in the release
|
|
131
|
+
// wires it to a publish, and until someone edits it by hand a fix shipped in
|
|
132
|
+
// the CLI reaches no onboarded repository at all — the gate keeps running the
|
|
133
|
+
// version named here. A published version behind the latest is reported the
|
|
134
|
+
// same way a stale action tag is.
|
|
135
|
+
const CLI_PIN = /REDLINE_CLI_VERSION:\s*'([^']+)'/g;
|
|
136
|
+
const cliPins = [];
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
const body = readFileSync(join(ROOT, file), 'utf8');
|
|
139
|
+
for (const [, version] of body.matchAll(CLI_PIN)) cliPins.push({ file, version });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (cliPins.length) {
|
|
143
|
+
const pinned = [...new Set(cliPins.map((p) => p.version))];
|
|
144
|
+
if (pinned.length > 1) {
|
|
145
|
+
console.error(
|
|
146
|
+
`FAIL REDLINE_CLI_VERSION disagrees with itself: ${pinned.join(', ')} — every job in one gate must run the same CLI`
|
|
147
|
+
);
|
|
148
|
+
errors += 1;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch('https://registry.npmjs.org/redlinegate');
|
|
152
|
+
if (!res.ok) throw new Error(`registry: ${res.status}`);
|
|
153
|
+
const latest = (await res.json())['dist-tags']?.latest;
|
|
154
|
+
for (const version of pinned) {
|
|
155
|
+
if (latest && latest !== version) {
|
|
156
|
+
console.warn(` update available: redlinegate@${version} → ${latest}`);
|
|
157
|
+
updates += 1;
|
|
158
|
+
} else {
|
|
159
|
+
console.log(`ok redlinegate@${version} is the published latest`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(`FAIL could not check redlinegate against the registry — ${err.message}`);
|
|
164
|
+
errors += 1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(
|
|
169
|
+
`\n${pins.length + cliPins.length + imagePins.length} pin(s), ${errors} error(s), ${updates} update(s) available`
|
|
170
|
+
);
|
|
88
171
|
process.exit(errors || (STRICT && updates) ? 1 : 0);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Context: spec-driven development
|
|
2
|
+
|
|
3
|
+
This repository works spec-first. A change begins as a specification, becomes a
|
|
4
|
+
plan, becomes tasks, and only then becomes code. Treat the specification as the
|
|
5
|
+
statement of intent that the diff is measured against.
|
|
6
|
+
|
|
7
|
+
When reviewing:
|
|
8
|
+
|
|
9
|
+
- Measure the diff against the specification it claims to implement, not against
|
|
10
|
+
what the code appears to be trying to do. Code that works and does something
|
|
11
|
+
the spec does not ask for is still a finding — say which section it departs
|
|
12
|
+
from.
|
|
13
|
+
- A change with no specification is not automatically wrong. Trivial fixes,
|
|
14
|
+
dependency bumps and revert commits do not need one. A new capability does.
|
|
15
|
+
- Where the specification and the code disagree, the specification is not
|
|
16
|
+
automatically right either. Say which one you believe is wrong and why, rather
|
|
17
|
+
than silently assuming the text wins.
|
|
18
|
+
- Do not restate the specification back to the author. They wrote it.
|
|
19
|
+
|
|
20
|
+
Spec Kit is a separate tool with its own installer and its own templates.
|
|
21
|
+
Redline does not create, edit or version its files; this section only tells a
|
|
22
|
+
reviewer that the repository works this way.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Context: TM Forum
|
|
2
|
+
|
|
3
|
+
This repository implements TM Forum interfaces. The published Open API and ODA
|
|
4
|
+
specifications are part of the contract, and a consumer integrating against them
|
|
5
|
+
is entitled to what those documents describe.
|
|
6
|
+
|
|
7
|
+
When reviewing:
|
|
8
|
+
|
|
9
|
+
- Resource and field names follow the published TMF API for the domain. A field
|
|
10
|
+
renamed for local convenience breaks every consumer generated from the spec.
|
|
11
|
+
- Polymorphism carries `@type`, and `@baseType` / `@schemaLocation` where the
|
|
12
|
+
API defines them. Dropping them because "nothing reads it here" removes the
|
|
13
|
+
discriminator a consumer needs.
|
|
14
|
+
- Collections page with `offset` and `limit`, filter through query parameters,
|
|
15
|
+
and honour `fields` for attribute selection. A hand-rolled paging scheme on a
|
|
16
|
+
TMF resource is a contract break.
|
|
17
|
+
- Errors use the TMF error body — `code`, `reason`, and `message` where present
|
|
18
|
+
— rather than a local error shape.
|
|
19
|
+
- Notifications follow the hub/listener pattern the API defines rather than a
|
|
20
|
+
bespoke webhook.
|
|
21
|
+
- A breaking change to a published interface needs a version, not an edit. Say
|
|
22
|
+
so explicitly when you see one.
|
|
23
|
+
|
|
24
|
+
State the specific TMF API and version when a finding depends on it, so the
|
|
25
|
+
author can check the same document you did.
|
package/standards/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "Single source of truth for Redline. scripts/render.mjs turns this into vendor-specific artifacts. Never hand-edit generated output.",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"core": {
|
|
5
5
|
"title": "Redline Core Engineering Standards",
|
|
6
6
|
"source": "standards/core.md"
|
package/templates/redline.yml
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
# Managed by Redline
|
|
2
|
-
# Redline caller. Installed into every onboarded repo as .github/workflows/redline.yml
|
|
3
|
-
# by `redline init`. Kept in sync by workflows/redline-sync.yml once `redline sync`
|
|
4
|
-
# ships in Phase 3 — see CHANGELOG.md.
|
|
1
|
+
# Managed by Redline. Regenerate with `redline init`; edits here are overwritten.
|
|
5
2
|
#
|
|
6
|
-
#
|
|
7
|
-
# which is
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
# Replace <org> with the organisation that hosts the `.github` repo.
|
|
3
|
+
# Keep the job id `redline-gate`: the branch ruleset requires the check
|
|
4
|
+
# `redline-gate / gate`, which is built from this id. Renaming it makes that
|
|
5
|
+
# check unreportable.
|
|
6
|
+
|
|
11
7
|
name: Redline
|
|
12
8
|
|
|
13
9
|
on:
|
package/workflows/dashboard.yml
CHANGED