Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
197 commits
Select commit Hold shift + click to select a range
4a3e858
test(ci): require bounded SARIF merge provenance
seonghobae Sep 11, 2026
ebedd2f
ci(test): run SARIF provenance RED verifier
seonghobae Sep 11, 2026
e3fabc8
fix(ci): materialize bounded SARIF PR provenance
seonghobae Sep 11, 2026
fc0dea4
ci: retire SARIF provenance verifier
seonghobae Sep 11, 2026
8d5feb5
ci(test): verify SARIF provenance candidate
seonghobae Sep 11, 2026
cb6f00f
ci(test): expose canonical SARIF provenance formatting
seonghobae Sep 11, 2026
65bb8c1
fix(ci): keep AppGuardrail evidence env ordering unchanged
seonghobae Sep 11, 2026
65aa16b
style(test): canonicalize SARIF provenance contract
seonghobae Sep 11, 2026
7c08c7f
ci(test): verify exact SARIF provenance candidate
seonghobae Sep 11, 2026
e4a7ddc
style(ci): preserve AppGuardrail newline boundary
seonghobae Sep 11, 2026
76e352e
ci: retire SARIF provenance full verifier
seonghobae Sep 11, 2026
169f18b
test(commercial): bound unnamed workflow sibling steps
seonghobae Sep 11, 2026
df0a74f
fix(commercial): bound workflow step extraction by indentation
seonghobae Sep 11, 2026
05179d5
test(commercial): enforce SARIF provenance guard and order
seonghobae Sep 11, 2026
4a0858a
test(commercial): use structural workflow step ordering
seonghobae Sep 11, 2026
fc50d20
test(commercial): reject duplicate workflow step authority
seonghobae Sep 11, 2026
02dd001
ci: verify SARIF step-name uniqueness regression
seonghobae Sep 11, 2026
4f550ba
test(commercial): require unique workflow step authority
seonghobae Sep 11, 2026
37412c6
ci: retire SARIF step uniqueness verifier
seonghobae Sep 11, 2026
a8f61be
test(ci): bind SARIF action authority to one reviewed step
seonghobae Sep 11, 2026
af22490
test(ci): reject unnamed duplicate SARIF action authority
seonghobae Sep 12, 2026
2a938ab
test(ci): ignore SARIF authority text inside run blocks
seonghobae Sep 12, 2026
d3e5ad2
test(ci): reproduce misplaced SARIF source binding
seonghobae Sep 12, 2026
592ac36
ci(test): run focused SARIF source-binding verifier
seonghobae Sep 12, 2026
ad014b6
test(ci): bind SARIF source identity to with inputs
seonghobae Sep 12, 2026
e68d9b1
ci(test): verify Commercial Readiness suite after SARIF repair
seonghobae Sep 12, 2026
6e25408
ci(test): retire focused SARIF verifier
seonghobae Sep 12, 2026
a608174
test(ci): reproduce duplicate SARIF source inputs
seonghobae Sep 12, 2026
acb1084
ci(test): verify duplicate SARIF source-input regression
seonghobae Sep 12, 2026
63db720
test(ci): reject duplicate SARIF source inputs
seonghobae Sep 12, 2026
7fbb68f
ci(test): verify package after duplicate-input repair
seonghobae Sep 12, 2026
ab72a88
ci(test): retire duplicate-input SARIF verifier
seonghobae Sep 12, 2026
a78ffc5
test(ci): prove SARIF upload job-boundary ambiguity
seonghobae Sep 12, 2026
db80a94
fix(ci): bind SARIF source verification to scan job
seonghobae Sep 12, 2026
df6b038
test(ci): reject SARIF job authority outside jobs
seonghobae Sep 12, 2026
f8ab2bc
fix(ci): bound SARIF job lookup to top-level jobs
seonghobae Sep 12, 2026
33d89f6
test(ci): reproduce SARIF provenance if-key false positive
seonghobae Sep 12, 2026
3a39971
test(ci): bind SARIF provenance guard to direct if key
seonghobae Sep 12, 2026
1e84e85
test(ci): reproduce provenance step-shape spoof
seonghobae Sep 12, 2026
8f9ccbc
test(ci): structurally bind provenance step authority
seonghobae Sep 12, 2026
779f265
test(ci): structurally bind provenance env authority
seonghobae Sep 12, 2026
a6f7266
test(ci): bind source checkout authority to owning job
seonghobae Sep 12, 2026
1404ce6
ci(test): verify source checkout authority contract
seonghobae Sep 12, 2026
9cf61e3
test(ci): retire source checkout verifier
seonghobae Sep 12, 2026
75a44ff
ci(test): verify checkout contract package quality
seonghobae Sep 12, 2026
b166eb5
ci(test): diagnose checkout contract formatting
seonghobae Sep 12, 2026
aa0dc70
ci(test): expose checkout contract formatter diff
seonghobae Sep 12, 2026
638f11b
style(test): normalize checkout authority contract
seonghobae Sep 12, 2026
6eae950
ci(test): verify normalized checkout contract package
seonghobae Sep 12, 2026
c30c2f9
ci(test): remove shallow-parent-only verifier check
seonghobae Sep 12, 2026
045f9db
test(ci): retire commercial readiness checkout verifier
seonghobae Sep 12, 2026
6616122
test(commercial): reject second source checkout
seonghobae Sep 12, 2026
9caaf1b
fix(commercial): bind source checkout authority
seonghobae Sep 12, 2026
3a83d1b
ci: add read-only maintainer verifier
seonghobae Sep 12, 2026
0e579fb
style(commercial): apply canonical verifier format
seonghobae Sep 12, 2026
8c4dc50
ci: retire maintainer verifier
seonghobae Sep 12, 2026
22d0e21
test(ci): reject case-variant self checkout authority
seonghobae Sep 12, 2026
5b5674e
test(ci): reject quoted checkout authority bypass
seonghobae Sep 12, 2026
b7ddafe
test(ci): parse commented quoted checkout scalars
seonghobae Sep 12, 2026
ea72605
test(ci): isolate external source checkouts
seonghobae Sep 12, 2026
9b80358
ci: verify external checkout path contract
seonghobae Sep 12, 2026
bab51bb
fix(ci): keep verifier checkout warning-free
seonghobae Sep 12, 2026
de53b8a
chore(ci): retire external checkout verifier
seonghobae Sep 12, 2026
31617bd
ci: verify commercial readiness path contract
seonghobae Sep 12, 2026
4546c84
chore(ci): retire commercial readiness verifier
seonghobae Sep 12, 2026
10dc22e
test(ci): add bounded external checkout verifier
seonghobae Sep 12, 2026
7f9d9b9
test(ci): reproduce external checkout source-subtree overwrite
seonghobae Sep 12, 2026
4a05f3a
test(ci): bind external checkout to reviewed isolation path
seonghobae Sep 12, 2026
cd50819
test(ci): retire external checkout verifier
seonghobae Sep 12, 2026
7647124
test(commercial): expose YAML alias checkout authority gap
seonghobae Sep 12, 2026
1088438
ci: verify source YAML alias regression
seonghobae Sep 12, 2026
52cbaf6
fix(commercial): reject YAML alias source authority
seonghobae Sep 12, 2026
7cf47da
test(commercial): expose noncanonical step sequence bypass
seonghobae Sep 12, 2026
0c83e6f
fix(commercial): require canonical direct source steps
seonghobae Sep 12, 2026
2e890df
ci: retire YAML alias verifier
seonghobae Sep 12, 2026
db36ec4
test(commercial): cover scalar YAML alias source authority
seonghobae Sep 12, 2026
5ab4fd1
ci(commercial): verify scalar YAML alias regression
seonghobae Sep 12, 2026
a3f39a6
fix(commercial): reject structural scalar YAML aliases
seonghobae Sep 12, 2026
30b9745
test(commercial): exercise nested scalar YAML aliases
seonghobae Sep 12, 2026
348a049
fix(commercial): inspect first-line scalar YAML authority
seonghobae Sep 12, 2026
d617e8a
ci(commercial): retire scalar YAML alias verifier
seonghobae Sep 12, 2026
6e8f80c
test(ci): bound provenance condition to direct job entries
seonghobae Sep 12, 2026
b91bf3b
test(ci): bound provenance env to direct job entries
seonghobae Sep 12, 2026
dac0537
test(ci): close YAML structural authority gaps
seonghobae Sep 12, 2026
9edf731
test(ci): pin external checkout path failure modes
seonghobae Sep 12, 2026
1db2110
ci: verify PR279 review repairs
seonghobae Sep 12, 2026
7be7d9a
test(ci): isolate mapping-key alias regression
seonghobae Sep 12, 2026
0f42fb2
ci: use Node 24 setup action in PR279 verifier
seonghobae Sep 12, 2026
6eb637a
ci: retire PR279 focused verifier
seonghobae Sep 12, 2026
f5dec48
test(commercial): reproduce quoted workflow-key escape
seonghobae Sep 12, 2026
6946fb5
test(commercial): reject quoted workflow authority keys
seonghobae Sep 12, 2026
a1283b3
test(commercial): reproduce explicit workflow-key escape
seonghobae Sep 12, 2026
f149bf7
test(commercial): reject explicit workflow authority keys
seonghobae Sep 12, 2026
74921ab
test(commercial): reproduce case-variant checkout action bypass
seonghobae Sep 12, 2026
3dce644
ci(commercial): add bounded checkout identity verifier
seonghobae Sep 12, 2026
4a9ef75
fix(commercial): normalize checkout action repository identity
seonghobae Sep 12, 2026
ac8a563
chore(ci): retire checkout identity verifier
seonghobae Sep 12, 2026
c0ab38a
test(commercial): cover spaced YAML structural keys
seonghobae Sep 12, 2026
6b18188
ci(commercial): verify spaced YAML structural-key regression
seonghobae Sep 12, 2026
d412afb
fix(commercial): reject spaced YAML structural keys
seonghobae Sep 12, 2026
e5a8e95
ci(commercial): prove spaced YAML key runtime semantics
seonghobae Sep 12, 2026
f2d5901
chore(ci): retire spaced-key runtime probe
seonghobae Sep 12, 2026
5d175a0
chore(ci): retire spaced-key contract verifier
seonghobae Sep 12, 2026
3c66a03
test(commercial): cover case-variant external checkout action
seonghobae Sep 12, 2026
ef0fabe
test(ci): verify external checkout action identity
seonghobae Sep 12, 2026
76492f1
fix(commercial): normalize checkout action repository identity
seonghobae Sep 12, 2026
7e798cb
chore(ci): retire external checkout identity verifier
seonghobae Sep 12, 2026
96718d6
test(ci): verify explicit YAML tag semantics
seonghobae Sep 12, 2026
b20a5a8
test(ci): reproduce YAML tag authority bypass
seonghobae Sep 12, 2026
5ec554c
test(ci): isolate YAML tag bypass RED
seonghobae Sep 12, 2026
20cb930
test(ci): run YAML tag bypass RED
seonghobae Sep 12, 2026
ccc0321
test(ci): correct YAML tag RED fixture
seonghobae Sep 12, 2026
52eb469
fix(ci): reject YAML tag authority in source verification
seonghobae Sep 12, 2026
c7053e3
test(ci): verify YAML tag authority repair
seonghobae Sep 12, 2026
882f344
chore(ci): retire YAML tag verifier
seonghobae Sep 12, 2026
324dc17
test(ci): run broad item 32 verification
seonghobae Sep 12, 2026
7290524
test(ci): repair broad verifier base materialization
seonghobae Sep 12, 2026
e773887
chore(ci): retire item 32 broad verifier
seonghobae Sep 12, 2026
19608e9
test(ci): expose final-step boundary contamination
seonghobae Sep 12, 2026
3522695
test(ci): host item 33 boundary RED
seonghobae Sep 12, 2026
3d219a1
fix(ci): bound final checkout step to steps section
seonghobae Sep 12, 2026
6d166bc
test(ci): broaden item 33 exact-head verification
seonghobae Sep 12, 2026
cc89f56
chore(ci): retire item 33 boundary verifier
seonghobae Sep 12, 2026
7a45a5c
test(ci): bound source verification at sibling jobs
seonghobae Sep 12, 2026
40f5b86
ci: verify item 34 source job boundary
seonghobae Sep 12, 2026
9524892
ci: retire item 34 verifier
seonghobae Sep 12, 2026
be47608
test(ci): reproduce SARIF sibling job authority leak
seonghobae Sep 12, 2026
a222182
fix(ci): bound SARIF authority to direct scan job
seonghobae Sep 12, 2026
416b240
test(ci): verify PR279 item35 exact contracts
seonghobae Sep 12, 2026
72cb172
test(ci): retire PR279 item35 verifier
seonghobae Sep 12, 2026
56fbca1
test(ci): add bounded verifier for PR 279 item 36
seonghobae Sep 12, 2026
0754494
test(ci): reproduce case-aliased SARIF action bypass
seonghobae Sep 12, 2026
7260b61
fix(ci): bind SARIF action repository identity case-insensitively
seonghobae Sep 12, 2026
91ef583
chore(ci): remove completed PR 279 item 36 verifier
seonghobae Sep 12, 2026
4b6ed63
test(ci): reproduce SARIF upload action identity bypass
seonghobae Sep 12, 2026
453b887
test(ci): verify SARIF upload action identity regression
seonghobae Sep 12, 2026
c750a0b
fix(ci): bind SARIF source evidence to upload action identity
seonghobae Sep 12, 2026
3e4defa
chore(ci): remove completed item 37 verifier
seonghobae Sep 12, 2026
78b5fb3
test(ci): bound SARIF upload lookup to direct steps
seonghobae Sep 13, 2026
b9c62e2
ci: verify PR279 item38 exact head
seonghobae Sep 13, 2026
ed0eb47
ci: retire PR279 item38 verifier
seonghobae Sep 13, 2026
9567333
test(ci): reproduce post-steps provenance guard authority leak
seonghobae Sep 13, 2026
5ae28a1
ci: add purpose verifier for PR279 item39
seonghobae Sep 13, 2026
f57d5fb
test(ci): bound provenance guard to direct steps
seonghobae Sep 13, 2026
42c3321
ci: make PR279 item39 verifier shallow-checkout safe
seonghobae Sep 13, 2026
90998d0
ci: make PR279 item39 hygiene check history independent
seonghobae Sep 13, 2026
ba96e16
ci: retire PR279 item39 verifier
seonghobae Sep 13, 2026
f012c7e
test(ci): reproduce post-steps provenance env authority leak
seonghobae Sep 13, 2026
83982dc
ci: add purpose verifier for PR279 item40
seonghobae Sep 13, 2026
80b3ea4
test(ci): bound provenance env to direct steps
seonghobae Sep 13, 2026
c4049ee
ci: retire PR279 item40 verifier
seonghobae Sep 13, 2026
c9db578
test(ci): reproduce source verifier steps-boundary borrowing
seonghobae Sep 13, 2026
26ed27a
fix(ci): bound source verifier to direct steps section
seonghobae Sep 13, 2026
d3df282
test(ci): broaden item 41 verifier
seonghobae Sep 13, 2026
ca2616a
test(ci): reproduce top-level job authority borrowing
seonghobae Sep 13, 2026
861e5cb
fix(ci): bound source verifier to top-level jobs mapping
seonghobae Sep 13, 2026
21d5a9c
test(ci): broaden item 42 verifier
seonghobae Sep 13, 2026
1dff787
chore(ci): remove completed PR 279 verifier
seonghobae Sep 13, 2026
7db2335
test(ci): expose provenance run marker false acceptance
seonghobae Sep 13, 2026
bf2adb3
ci(test): verify PR 279 item 43 regression
seonghobae Sep 13, 2026
3d7b53c
fix(ci): bind provenance to exact direct run authority
seonghobae Sep 13, 2026
070f29b
ci(test): broaden PR 279 item 43 verification
seonghobae Sep 13, 2026
45259ab
chore(ci): remove completed PR 279 item 43 verifier
seonghobae Sep 13, 2026
4ef8d98
fix(ci): honor provenance run scalar separator blanks
seonghobae Sep 13, 2026
682f092
ci(test): verify PR 279 item 43 repair
seonghobae Sep 13, 2026
85e23cd
ci(test): retire PR 279 item 43 verifier
seonghobae Sep 13, 2026
5ba263d
ci(test): verify item 43 with bounded hygiene history
seonghobae Sep 13, 2026
305fb99
ci(test): retire item 43 bounded verifier
seonghobae Sep 13, 2026
7e5d39f
test(ci): reproduce provenance run step impersonation
seonghobae Sep 13, 2026
22d728d
ci: verify PR 279 item 44 regression
seonghobae Sep 13, 2026
92b553b
test(ci): bound provenance run step authority to scan steps
seonghobae Sep 13, 2026
cbf486d
ci: broaden PR 279 item 44 verification
seonghobae Sep 13, 2026
b1c6550
ci: remove completed PR 279 item 44 verifier
seonghobae Sep 13, 2026
cd6c5c3
test(ci): prove SARIF upload provenance gate
seonghobae Sep 13, 2026
6afd315
test(ci): run PR279 Item45 regression
seonghobae Sep 13, 2026
1f9f37b
fix(ci): fail closed before SARIF upload
seonghobae Sep 13, 2026
697f331
test(ci): verify PR279 Item45 repair
seonghobae Sep 13, 2026
04ac6be
chore(ci): retire PR279 Item45 verifier
seonghobae Sep 13, 2026
03ec520
merge: restack PR279 onto PR276 checkout contract
seonghobae Sep 13, 2026
a087f11
chore(stack): inherit checkout-step authority contract
seonghobae Sep 13, 2026
8374939
chore(stack): merge latest Node24 contract parent
seonghobae Sep 13, 2026
e2807eb
chore(stack): inherit block-scalar checkout authority repair
seonghobae Sep 13, 2026
08a5e76
chore(stack): merge latest Node24 contract parent
seonghobae Sep 13, 2026
4389cb0
chore(stack): merge checkout authority hardening parent
seonghobae Sep 13, 2026
5e602af
chore(stack): merge checkout step-boundary hardening parent
seonghobae Sep 13, 2026
295548b
chore(stack): adopt checkout env authority hardening parent
seonghobae Sep 13, 2026
e476193
chore(stack): adopt action-pin case authority hardening
seonghobae Sep 13, 2026
9b765d1
chore(stack): adopt quoted checkout authority hardening
seonghobae Sep 13, 2026
c537bfb
chore(stack): adopt contract newline cleanup
seonghobae Sep 13, 2026
2430aad
chore(stack): adopt AppGuardrail jobs-boundary contract
seonghobae Sep 13, 2026
3c720ce
chore(stack): adopt direct-sequence checkout contract
seonghobae Sep 13, 2026
0de1bb0
chore(stack): adopt multi-space checkout contract
seonghobae Sep 13, 2026
be26085
chore(stack): adopt quoted uses-key contract
seonghobae Sep 14, 2026
27437b8
chore(stack): adopt quoted uses authority docs
seonghobae Sep 14, 2026
86fcdfa
chore(stack): adopt Node24 flow-style authority contract
seonghobae Sep 15, 2026
f8b5447
chore(stack): adopt quoted flow-style action authority
seonghobae Sep 15, 2026
f519909
chore(stack): adopt flow-style action key boundary
seonghobae Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion .github/workflows/appguardrail.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,41 @@ jobs:
)
PY

- name: Materialize AppGuardrail SARIF PR merge provenance
id: materialize_pr_provenance
if: >-
github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.full_name == github.repository
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
EXPECTED_MERGE_SHA: ${{ github.sha }}
run: |
set -euo pipefail
if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::Pull request number is not a positive integer."
exit 1
fi
if ! [[ "$EXPECTED_MERGE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::Advertised pull request merge SHA is not a full commit SHA."
exit 1
fi

merge_ref="refs/pull/${PR_NUMBER}/merge"
git fetch --no-tags --depth=1 origin "$merge_ref"
fetched_merge_sha="$(git rev-parse FETCH_HEAD)"
if [ "$fetched_merge_sha" != "$EXPECTED_MERGE_SHA" ]; then
echo "::error::Fetched pull request merge provenance does not match github.sha."
exit 1
fi
git cat-file -e "${EXPECTED_MERGE_SHA}^{commit}"

- name: Upload AppGuardrail SARIF to code scanning
if: >-
always()
&& hashFiles('appguardrail.sarif') != ''
&& (github.event_name != 'pull_request'
|| github.event.pull_request.head.repo.full_name == github.repository)
|| (github.event.pull_request.head.repo.full_name == github.repository
&& steps.materialize_pr_provenance.outcome == 'success'))
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
sarif_file: appguardrail.sarif
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { join } from 'node:path';
import test from 'node:test';

const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url));
const PROVENANCE_STEP_NAME =
'Materialize AppGuardrail SARIF PR merge provenance';
const PULL_REQUEST_CONDITION = "github.event_name == 'pull_request'";
const SAME_REPOSITORY_CONDITION =
'github.event.pull_request.head.repo.full_name == github.repository';
const EXPECTED_PROVENANCE_CONDITION = `${PULL_REQUEST_CONDITION} && ${SAME_REPOSITORY_CONDITION}`;

/** Finds direct jobs while respecting quoted IDs and inline-comment boundaries. */
function directJobEntries(lines, start, end) {
const entries = [];
const pattern =
/^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u;
for (let index = start; index < end; index += 1) {
const match = pattern.exec(lines[index]);
if (match) {
entries.push({ index, name: match[1] ?? match[2] ?? match[3] });
}
}
return entries;
}

/** Extracts one uniquely named direct job from the workflow's top-level jobs mapping. */
function namedJob(workflow, jobName) {
const lines = workflow.split('\n');
const jobsIndexes = [];
for (let index = 0; index < lines.length; index += 1) {
if (lines[index] === 'jobs:') {
jobsIndexes.push(index);
}
}
assert.equal(jobsIndexes.length, 1, 'workflow must contain exactly one jobs mapping');

const jobsStart = jobsIndexes[0];
let jobsEnd = lines.length;
for (let index = jobsStart + 1; index < lines.length; index += 1) {
if (/^[^\s#]/u.test(lines[index])) {
jobsEnd = index;
break;
}
}

const entries = directJobEntries(lines, jobsStart + 1, jobsEnd);
const matches = entries.filter((entry) => entry.name === jobName);
assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`);

const start = matches[0].index;
const position = entries.findIndex((entry) => entry.index === start);
const end =
position + 1 < entries.length ? entries[position + 1].index : jobsEnd;
return lines.slice(start, end).join('\n');
}

/** Extracts one uniquely named real step from the direct steps sequence of jobs.scan. */
function namedStep(workflow, stepName) {
const scanJob = namedJob(workflow, 'scan');
const lines = scanJob.split('\n');
const jobMatch = /^(\s*)scan:\s*$/u.exec(lines[0]);
assert.ok(jobMatch, 'scan job indentation is invalid');
const stepsLine = `${jobMatch[1]} steps:`;
const stepsIndexes = [];
for (let index = 1; index < lines.length; index += 1) {
if (lines[index] === stepsLine) {
stepsIndexes.push(index);
}
}
assert.equal(
stepsIndexes.length,
1,
'scan job must contain exactly one direct steps mapping',
);

const stepsStart = stepsIndexes[0];
const directJobMemberIndent = `${jobMatch[1]} `;
const stepIndent = `${jobMatch[1]} `;
let stepsEnd = lines.length;
for (let index = stepsStart + 1; index < lines.length; index += 1) {
const line = lines[index];
const trimmed = line.trim();
if (trimmed === '' || trimmed.startsWith('#')) {
continue;
}
if (
line.startsWith(directJobMemberIndent) &&
!line.startsWith(stepIndent)
) {
stepsEnd = index;
break;
}
}

const expected = `${stepIndent}- name: ${stepName}`;
const matches = [];
for (let index = stepsStart + 1; index < stepsEnd; index += 1) {
if (lines[index] === expected) {
matches.push(index);
}
}
assert.equal(
matches.length,
1,
`expected exactly one direct workflow step ${stepName}`,
);

const start = matches[0];
let end = stepsEnd;
for (let index = start + 1; index < stepsEnd; index += 1) {
if (lines[index].startsWith(`${stepIndent}- `)) {
end = index;
break;
}
}
return lines.slice(start, end).join('\n');
}

/** Reads one unique direct scalar key from a workflow step and normalizes folded block text. */
function directScalar(step, key) {
const lines = step.split('\n');
const stepMatch = /^(\s*)-\s/u.exec(lines[0]);
assert.ok(stepMatch, 'workflow step indentation is invalid');
const directIndent = `${stepMatch[1]} `;
const prefix = `${directIndent}${key}:`;
const matches = [];

for (let index = 1; index < lines.length; index += 1) {
if (!lines[index].startsWith(prefix)) {
continue;
}
const suffix = lines[index].slice(prefix.length);
if (suffix.length === 0 || /^\s/u.test(suffix)) {
matches.push({ index, suffix: suffix.trim() });
}
}
assert.equal(matches.length, 1, `expected exactly one direct ${key} key`);

const { index, suffix } = matches[0];
if (!/^[>|][+-]?$/u.test(suffix)) {
return suffix;
}

const body = [];
for (let bodyIndex = index + 1; bodyIndex < lines.length; bodyIndex += 1) {
const line = lines[bodyIndex];
if (line.trim().length === 0) {
continue;
}
const leading = /^\s*/u.exec(line)?.[0].length ?? 0;
if (leading <= directIndent.length) {
break;
}
body.push(line.trim());
}
assert.notEqual(body.length, 0, `direct ${key} block scalar must not be empty`);
return body.join(' ');
}

/** Requires the provenance restriction to live in the step's direct if key. */
function assertProvenanceGuard(step) {
assert.equal(
directScalar(step, 'if'),
EXPECTED_PROVENANCE_CONDITION,
'provenance step must require pull_request and same-repository authority in its direct if guard',
);
}

test('AppGuardrail provenance step owns the exact same-repository pull_request guard', () => {
const workflow = readFileSync(
join(REPOSITORY_ROOT, '.github/workflows/appguardrail.yml'),
'utf8',
);
assert.doesNotThrow(() =>
assertProvenanceGuard(namedStep(workflow, PROVENANCE_STEP_NAME)),
);
});

test('provenance condition rejects guard strings that exist only outside the if key', () => {
const hostileStep = [
` - name: ${PROVENANCE_STEP_NAME}`,
' if: always()',
` # ${PULL_REQUEST_CONDITION}`,
' run: |',
` echo "${SAME_REPOSITORY_CONDITION}"`,
].join('\n');

assert.throws(
() => assertProvenanceGuard(hostileStep),
/direct if guard/u,
'comments or run-block text must not satisfy the provenance guard contract',
);
});

test('provenance condition rejects duplicate direct if authority', () => {
const hostileStep = [
` - name: ${PROVENANCE_STEP_NAME}`,
' if: >-',
` ${PULL_REQUEST_CONDITION}`,
` && ${SAME_REPOSITORY_CONDITION}`,
' if: always()',
' run: echo duplicate-if',
].join('\n');

assert.throws(
() => assertProvenanceGuard(hostileStep),
/exactly one direct if key/u,
);
});

test('provenance step authority rejects step-shaped text inside a run block', () => {
const hostileWorkflow = [
'jobs:',
' scan:',
' steps:',
' - name: Harmless generator',
' run: |',
" cat <<'EOF' > note.yml",
` - name: ${PROVENANCE_STEP_NAME}`,
' if: >-',
` ${PULL_REQUEST_CONDITION}`,
` && ${SAME_REPOSITORY_CONDITION}`,
' run: echo fake-authority',
' EOF',
].join('\n');

assert.throws(
() =>
assertProvenanceGuard(namedStep(hostileWorkflow, PROVENANCE_STEP_NAME)),
/direct workflow step/u,
'run-block text must not become provenance workflow-step authority',
);
});

test('provenance condition rejects step-shaped text after the direct steps sequence', () => {
const hostileWorkflow = [
'jobs:',
' scan:',
' steps:',
' - name: Harmless scan step',
' run: echo scan',
' name: |',
` - name: ${PROVENANCE_STEP_NAME}`,
' if: >-',
` ${PULL_REQUEST_CONDITION}`,
` && ${SAME_REPOSITORY_CONDITION}`,
' run: echo fake-authority',
].join('\n');

assert.throws(
() =>
assertProvenanceGuard(namedStep(hostileWorkflow, PROVENANCE_STEP_NAME)),
/direct workflow step/u,
'job-level mappings after steps must not lend provenance-step authority',
);
});

test('provenance condition does not borrow authority from quoted or commented sibling jobs', () => {
for (const sibling of [' decoy: # sibling', ' "decoy":']) {
const hostileWorkflow = [
'jobs:',
' scan:',
sibling,
' steps:',
` - name: ${PROVENANCE_STEP_NAME}`,
' if: >-',
` ${PULL_REQUEST_CONDITION}`,
` && ${SAME_REPOSITORY_CONDITION}`,
' run: echo sibling-authority',
].join('\n');

assert.throws(
() =>
assertProvenanceGuard(namedStep(hostileWorkflow, PROVENANCE_STEP_NAME)),
/direct steps mapping/u,
'a sibling job must not lend provenance guard authority to jobs.scan',
);
}
});
Loading