-
Notifications
You must be signed in to change notification settings - Fork 0
fix(security): enforce consistent action pin annotations #2093
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
seonghobae
wants to merge
1
commit into
main
Choose a base branch
from
fix/action-pin-annotation-integrity-1543
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Validate that immutable GitHub Actions pins use one consistent release annotation across the repository, and correct two stale annotations. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| """Keep release annotations consistent for immutable GitHub Action pins.""" | ||
|
|
||
| from collections import defaultdict | ||
| from pathlib import Path | ||
| import re | ||
|
|
||
|
|
||
| ROOT = Path(__file__).resolve().parents[1] | ||
| UPLOAD_ARTIFACT_SHA = "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" | ||
| OSV_SCANNER_SHA = "8e5cf47b818121e8b405931c82126c2630b0b20d" | ||
| ACTION_PIN = re.compile( | ||
| r"^\s*uses:\s+(?P<action>[^\s@]+)@(?P<sha>[0-9a-f]{40})\s+#\s*(?P<annotation>\S.*?)\s*$" | ||
| ) | ||
| IMMUTABLE_PIN = re.compile(r"^\s*uses:\s+[^\s@]+@[0-9a-f]{40}(?:\s|$)") | ||
| ACTION_REFERENCE = re.compile(r"^\s*uses:\s+(?P<action>[^\s@]+)@(?P<ref>[^\s#]+)") | ||
| RELEASE_ANNOTATION = re.compile(r"^v[0-9]+(?:\.[0-9]+)*(?:[-+][0-9A-Za-z.-]+)?$") | ||
|
|
||
|
|
||
| def _action_files() -> list[Path]: | ||
| """Return workflow and composite-action YAML files.""" | ||
|
|
||
| return sorted( | ||
| path | ||
| for directory in (ROOT / ".github" / "workflows", ROOT / ".github" / "actions") | ||
| for path in (*directory.rglob("*.yml"), *directory.rglob("*.yaml")) | ||
| ) | ||
|
|
||
|
|
||
| def test_each_action_pin_sha_has_one_release_annotation() -> None: | ||
| """The same immutable action identity must not advertise different releases.""" | ||
|
|
||
| annotations: defaultdict[tuple[str, str], list[tuple[str, int, str]]] = defaultdict( | ||
| list | ||
| ) | ||
| for path in _action_files(): | ||
| for line_number, line in enumerate( | ||
| path.read_text(encoding="utf-8").splitlines(), start=1 | ||
| ): | ||
| match = ACTION_PIN.match(line) | ||
| if match: | ||
| identity = (match["action"], match["sha"]) | ||
| annotations[identity].append( | ||
| (str(path.relative_to(ROOT)), line_number, match["annotation"]) | ||
| ) | ||
|
|
||
| contradictions = { | ||
| identity: locations | ||
| for identity, locations in annotations.items() | ||
| if len({annotation for _, _, annotation in locations}) > 1 | ||
| } | ||
| assert not contradictions, "contradictory immutable action annotations: " + repr( | ||
| contradictions | ||
| ) | ||
|
|
||
|
|
||
| def test_each_immutable_action_pin_has_a_release_annotation() -> None: | ||
| """Do not let an unlabelled immutable pin evade the integrity checks.""" | ||
|
|
||
| missing: list[tuple[str, int, str]] = [] | ||
| for path in _action_files(): | ||
| for line_number, line in enumerate( | ||
| path.read_text(encoding="utf-8").splitlines(), start=1 | ||
| ): | ||
| if IMMUTABLE_PIN.match(line) and not ACTION_PIN.match(line): | ||
| missing.append((str(path.relative_to(ROOT)), line_number, line.strip())) | ||
|
|
||
| assert not missing, "immutable action pins need release annotations: " + repr(missing) | ||
|
|
||
|
|
||
| def test_each_external_action_reference_is_immutable() -> None: | ||
| """Do not permit mutable tags or branches for third-party actions.""" | ||
|
|
||
| mutable: list[tuple[str, int, str]] = [] | ||
| for path in _action_files(): | ||
| for line_number, line in enumerate( | ||
| path.read_text(encoding="utf-8").splitlines(), start=1 | ||
| ): | ||
| match = ACTION_REFERENCE.match(line) | ||
| if ( | ||
| match | ||
| and not match["action"].startswith("./") | ||
| and not re.fullmatch(r"[0-9a-f]{40}", match["ref"]) | ||
| ): | ||
| mutable.append((str(path.relative_to(ROOT)), line_number, line.strip())) | ||
|
|
||
| assert not mutable, "external actions must use immutable commit SHAs: " + repr(mutable) | ||
|
|
||
|
|
||
| def test_action_pin_annotations_are_release_shaped() -> None: | ||
| """Reject labels that cannot identify a concrete released revision.""" | ||
|
|
||
| malformed: list[tuple[str, int, str]] = [] | ||
| for path in _action_files(): | ||
| for line_number, line in enumerate( | ||
| path.read_text(encoding="utf-8").splitlines(), start=1 | ||
| ): | ||
| match = ACTION_PIN.match(line) | ||
| if match and not RELEASE_ANNOTATION.fullmatch(match["annotation"]): | ||
| malformed.append( | ||
| (str(path.relative_to(ROOT)), line_number, match["annotation"]) | ||
| ) | ||
|
|
||
| assert not malformed, "action pin annotations must identify a release: " + repr( | ||
| malformed | ||
| ) | ||
|
|
||
|
|
||
| def test_action_subpaths_share_the_repository_sha_annotation() -> None: | ||
| """Sub-actions from one immutable repository revision share one label.""" | ||
|
|
||
| annotations: defaultdict[tuple[str, str], set[str]] = defaultdict(set) | ||
| for directory in (ROOT / ".github" / "workflows", ROOT / ".github" / "actions"): | ||
| for path in sorted((*directory.rglob("*.yml"), *directory.rglob("*.yaml"))): | ||
| for line in path.read_text(encoding="utf-8").splitlines(): | ||
| match = ACTION_PIN.match(line) | ||
| if match: | ||
| repository = "/".join(match["action"].split("/")[:2]) | ||
| annotations[(repository, match["sha"])].add(match["annotation"]) | ||
|
|
||
| contradictions = { | ||
| identity: sorted(values) | ||
| for identity, values in annotations.items() | ||
| if len(values) > 1 | ||
| } | ||
| assert not contradictions, "contradictory repository pin annotations: " + repr( | ||
| contradictions | ||
| ) | ||
|
|
||
|
|
||
| def test_upload_artifact_uses_the_reviewed_node24_pin() -> None: | ||
| """Keep artifact uploads off the deprecated Node 20 action runtime.""" | ||
|
|
||
| upload_pins = [] | ||
| for path in _action_files(): | ||
| for line_number, line in enumerate( | ||
| path.read_text(encoding="utf-8").splitlines(), start=1 | ||
| ): | ||
| match = ACTION_PIN.match(line) | ||
| if match and match["action"] == "actions/upload-artifact": | ||
| upload_pins.append((path, line_number, match["sha"])) | ||
|
|
||
| assert upload_pins | ||
| assert all(sha == UPLOAD_ARTIFACT_SHA for _, _, sha in upload_pins), upload_pins | ||
|
|
||
|
|
||
| def test_osv_action_subpaths_use_one_current_upstream_pin() -> None: | ||
| """Keep scanner and reporter sub-actions on one fail-closed revision.""" | ||
|
|
||
| osv_pins = [] | ||
| for path in _action_files(): | ||
| for line_number, line in enumerate( | ||
| path.read_text(encoding="utf-8").splitlines(), start=1 | ||
| ): | ||
| match = ACTION_PIN.match(line) | ||
| if match and match["action"].startswith("google/osv-scanner-action/"): | ||
| osv_pins.append((path, line_number, match["action"], match["sha"])) | ||
|
|
||
| assert osv_pins | ||
| assert all(sha == OSV_SCANNER_SHA for _, _, _, sha in osv_pins), osv_pins | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: ContextualWisdomLab/.github
Length of output: 12080
🤖 get_repo_knowledge executed:
get_repo_knowledge ContextualWisdomLab/.github /tmp/coderabbit-repo-knowledge/contextualwisdomlab-github-bad40478/conventionsLength of output: 13578
🏁 Script executed:
Repository: ContextualWisdomLab/.github
Length of output: 12026
🏁 Script executed:
Repository: ContextualWisdomLab/.github
Length of output: 10871
🏁 Script executed:
Repository: ContextualWisdomLab/.github
Length of output: 1631
Security Misconfiguration
Reachability: External
Exploitability: Theoretical
CWE: CWE-345
각 immutable SHA와 릴리스 주석의 정확한 매핑을 계약 테스트에 고정하세요.
현재 테스트는 SHA와 주석 형식·일관성만 검사합니다. 다음 매핑을 명시적으로 검증해야 합니다.
actions/upload-artifact→v7.0.1google/osv-scanner-action/osv-scanner-action및google/osv-scanner-action/osv-reporter-action→v2.5.1-6-g8e5cf47.github/workflows/organization-commercial-readiness-loop.yml의step-security/harden-runner→v2.20.0🤖 Prompt for AI Agents