mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Close workflow trust policy gaps
This commit is contained in:
@@ -22,9 +22,10 @@ Workflows triggered by `pull_request` cannot reference confidential repository
|
||||
secrets. Canonical governance therefore keeps its pull-request checks local to
|
||||
the public checkout. `canonical-private-governance.yml` performs cross-repo
|
||||
status, control-plane, mobile compatibility, and repo-governance checks only
|
||||
after a push to `main`, so pull-request code cannot replace the instructions
|
||||
that receive `WORKFLOW_PAT`. `PULSE_LICENSE_PUBLIC_KEY` is the sole explicit PR
|
||||
exception because that legacy secret value is intentionally non-confidential.
|
||||
after a push to `main`, so unmerged pull-request code cannot replace the
|
||||
instructions that receive `WORKFLOW_PAT`. `PULSE_LICENSE_PUBLIC_KEY` is the sole
|
||||
explicit PR exception because that legacy secret value is intentionally
|
||||
non-confidential.
|
||||
|
||||
## Release Continuity
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ jobs:
|
||||
governance:
|
||||
runs-on: ubuntu-24.04
|
||||
# Keep pull-request governance free of repository credentials. Cross-repo
|
||||
# evidence checks run after merge in canonical-private-governance.yml,
|
||||
# whose workflow definition cannot be replaced by pull-request code.
|
||||
# evidence checks run after merge in canonical-private-governance.yml, so
|
||||
# its credential-bearing job never executes from an unmerged PR revision.
|
||||
defaults:
|
||||
run:
|
||||
working-directory: repos/pulse
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
name: Canonical Private Governance
|
||||
|
||||
# Repository credentials are deliberately confined to a workflow definition
|
||||
# loaded by a push to main. Pull-request code must never be able to replace the
|
||||
# instructions that receive WORKFLOW_PAT or execute with private checkouts.
|
||||
# loaded by a push to main. Unmerged pull-request code must never be able to
|
||||
# replace the instructions that receive WORKFLOW_PAT or execute with private
|
||||
# checkouts.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
||||
@@ -447,10 +447,10 @@ repository. It classifies every declared capability as `mobile-required`,
|
||||
scopes, required request/response fields, pairing data, and push navigation;
|
||||
and generates both the Go runtime inventory and Pulse Mobile TypeScript
|
||||
projection. `pulse-mobile:config/mobile-api-surface.json` is the consumer
|
||||
minimum and released-line probe declaration. Canonical Governance must compare
|
||||
that consumer against the exact Pulse manifest and retain revision-bound
|
||||
evidence. Pulse Mobile's OTA gate separately proves the app against server
|
||||
lines already in customers' hands.
|
||||
minimum and released-line probe declaration. Canonical Private Governance must
|
||||
compare that consumer against the exact Pulse manifest and retain
|
||||
revision-bound evidence. Pulse Mobile's OTA gate separately proves the app
|
||||
against server lines already in customers' hands.
|
||||
|
||||
Proxy-auth administrator evaluation is a shared auth/API contract. Once
|
||||
`PROXY_AUTH_ROLE_HEADER` is configured, `internal/api/auth.go` must treat a
|
||||
|
||||
@@ -111,7 +111,7 @@ dark-site signal and must not be conflated with this per-agent alert.
|
||||
5. Keep mobile relay runtime changes tied to explicit proof in `pulse-mobile:src/relay/__tests__/`
|
||||
6. Keep the operator Relay incapable of serving v6 grants until it has synchronously drained the authenticated revocation feed, and expose stale feed state through readiness.
|
||||
7. Keep feed-applied restrictive events tied to proof that already-connected stale grants are disconnected and their persisted reconnect credentials are invalidated.
|
||||
8. Keep exact-revision Pulse/Pulse Mobile compatibility evidence green in Canonical Governance and keep released-line compatibility green in the Pulse Mobile OTA gate.
|
||||
8. Keep exact-revision Pulse/Pulse Mobile compatibility evidence green in Canonical Private Governance and keep released-line compatibility green in the Pulse Mobile OTA gate.
|
||||
9. Keep server-side mobile operational metric changes tied to Relay bridge, device-store, and metric contract tests. These aggregate service signals must not be presented as unique-user, retention, app-open, screen-view, or feature-usage analytics.
|
||||
|
||||
## Current State
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user