mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Close workflow trust policy gaps
This commit is contained in:
parent
21007a8662
commit
a0dfdadc5e
7 changed files with 36 additions and 23 deletions
|
|
@ -20,7 +20,7 @@ EXPRESSION_RE = re.compile(r"\$\{\{(.*?)\}\}")
|
|||
# github.token because Actions makes that credential available independently of
|
||||
# an explicit secrets.GITHUB_TOKEN reference.
|
||||
SHELL_DATA_CONTEXT_RE = re.compile(
|
||||
r"(?<![\w.])(?:inputs|secrets)(?:\.|\s*\[)|(?<![\w.])github\.token\b"
|
||||
r"(?<![\w.])(?:inputs|secrets)\b|(?<![\w.])github\.token\b"
|
||||
)
|
||||
# GitHub documents these event fields as attacker-controlled strings. They may
|
||||
# be passed through env, but interpolating them into a generated shell program
|
||||
|
|
@ -38,9 +38,7 @@ SECRET_CONTEXT_RE = re.compile(
|
|||
r"\[\s*(['\"])([A-Za-z_][A-Za-z0-9_]*)\2\s*\]"
|
||||
r")"
|
||||
)
|
||||
DYNAMIC_SECRET_CONTEXT_RE = re.compile(
|
||||
r"(?<![\w.])secrets\s*\[(?!\s*['\"][A-Za-z_][A-Za-z0-9_]*['\"]\s*\])"
|
||||
)
|
||||
SECRET_CONTEXT_TOKEN_RE = re.compile(r"(?<![\w.])secrets\b")
|
||||
# This value is intentionally public and only uses secret storage as a legacy
|
||||
# configuration mechanism. Confidential credentials have no PR exception.
|
||||
NON_CONFIDENTIAL_PULL_REQUEST_SECRETS = frozenset({"PULSE_LICENSE_PUBLIC_KEY"})
|
||||
|
|
@ -127,13 +125,24 @@ def audit_workflow(path: Path) -> list[Finding]:
|
|||
if _has_trigger(lines, "pull_request"):
|
||||
for index, line in enumerate(lines):
|
||||
code = line.split("#", 1)[0]
|
||||
secret_names = {
|
||||
match.group(1) or match.group(3)
|
||||
for match in SECRET_CONTEXT_RE.finditer(code)
|
||||
}
|
||||
expressions = EXPRESSION_RE.findall(code)
|
||||
secret_names: set[str] = set()
|
||||
has_unresolved_secret_reference = False
|
||||
for expression in expressions:
|
||||
static_references = list(SECRET_CONTEXT_RE.finditer(expression))
|
||||
secret_names.update(
|
||||
match.group(1) or match.group(3) for match in static_references
|
||||
)
|
||||
if len(SECRET_CONTEXT_TOKEN_RE.findall(expression)) != len(
|
||||
static_references
|
||||
):
|
||||
# Whole-context and dynamic references can expose any
|
||||
# repository secret, so they cannot use the public-key
|
||||
# exception reserved for a statically named value.
|
||||
has_unresolved_secret_reference = True
|
||||
if (
|
||||
secret_names - NON_CONFIDENTIAL_PULL_REQUEST_SECRETS
|
||||
or DYNAMIC_SECRET_CONTEXT_RE.search(code)
|
||||
or has_unresolved_secret_reference
|
||||
):
|
||||
findings.append(
|
||||
Finding(
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ jobs:
|
|||
steps:
|
||||
- run: echo "${{ inputs.name }}"
|
||||
- run: echo "${{ inputs['name'] }}"
|
||||
- run: echo "${{ toJSON(secrets) }}"
|
||||
- run: |
|
||||
echo "${{ secrets.ACCESS_TOKEN }}"
|
||||
echo "${{ github.token }}"
|
||||
|
|
@ -133,7 +134,7 @@ jobs:
|
|||
)
|
||||
self.assertEqual(
|
||||
sum("must enter run scripts through env" in finding for finding in findings),
|
||||
4,
|
||||
5,
|
||||
)
|
||||
|
||||
def test_rejects_untrusted_github_metadata_in_generated_shell(self) -> None:
|
||||
|
|
@ -173,6 +174,7 @@ jobs:
|
|||
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
|
||||
GH_TOKEN_BRACKET: ${{ secrets['WORKFLOW_PAT'] }}
|
||||
DYNAMIC_SECRET: ${{ secrets[vars.SECRET_NAME] }}
|
||||
WHOLE_SECRET_CONTEXT: ${{ toJSON(secrets) }}
|
||||
PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
PUBLIC_KEY_BRACKET: ${{ secrets['PULSE_LICENSE_PUBLIC_KEY'] }}
|
||||
run: echo checked
|
||||
|
|
@ -180,7 +182,7 @@ jobs:
|
|||
)
|
||||
self.assertEqual(
|
||||
sum("must not reference confidential repository secrets" in finding for finding in findings),
|
||||
3,
|
||||
4,
|
||||
)
|
||||
|
||||
trusted_push = self.audit(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue