Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
222 changes: 222 additions & 0 deletions .github/workflows/public-token-free-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,228 @@ jobs:
with:
python-version: "3.13"

- name: Enforce public repository boundary
shell: bash
run: |
set -euo pipefail
python <<'PY'
from pathlib import Path
import re
import sys

root = Path(".")
allowed_runners = {
"ubuntu-slim",
"ubuntu-latest",
"ubuntu-22.04",
"ubuntu-24.04",
"ubuntu-26.04",
"ubuntu-22.04-arm",
"ubuntu-24.04-arm",
"ubuntu-26.04-arm",
"windows-latest",
"windows-2022",
"windows-2025",
"windows-2025-vs2026",
"windows-11-arm",
"windows-11-vs2026-arm",
"macos-latest",
"macos-14",
"macos-15",
"macos-15-intel",
"macos-26",
"macos-26-intel",
}
ignored_parts = {
".git",
".quality",
".venv",
"coverage",
"dist",
"node_modules",
"vendor",
}
package_secret_names = {
"GHCR_TOKEN",
"GITHUB_PACKAGES_TOKEN",
"NODE_AUTH_TOKEN",
"NPM_AUTH_TOKEN",
"NPM_TOKEN",
"PACKAGE_TOKEN",
"PACKAGES_TOKEN",
}
dependency_file_names = {
"cargo.lock",
"cargo.toml",
"composer.json",
"composer.lock",
"go.mod",
"go.sum",
"package-lock.json",
"package.json",
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
"poetry.lock",
"pyproject.toml",
"uv.lock",
"yarn.lock",
}
expression_start = "$" + "{{"
errors = []

def add_error(path: Path, text: str, position: int, message: str) -> None:
line = text.count("\n", 0, position) + 1
errors.append(f"{path.as_posix()}:{line}: {message}")

def read_text_files():
for path in sorted(root.rglob("*")):
if not path.is_file() or ignored_parts.intersection(path.parts):
continue
if path.stat().st_size > 2_000_000:
continue
try:
yield path, path.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue

def is_dependency_file(path: Path) -> bool:
name = path.name.lower()
return (
name in dependency_file_names
or name.startswith(("dockerfile", "gemfile", "pipfile", "requirements"))
or name.endswith((".gradle", ".gradle.kts"))
)

files = list(read_text_files())
workflows = [
(path, text)
for path, text in files
if path.parent.as_posix().endswith(".github/workflows")
and path.suffix.lower() in {".yaml", ".yml"}
]

runner_pattern = re.compile(
r"^\s*['\"]?runs-on['\"]?\s*:\s*(.*?)\s*(?:#.*)?$",
re.MULTILINE,
)
event_pattern = re.compile(
r"^\s*['\"]?pull_request_target['\"]?\s*:", re.MULTILINE
)
uses_pattern = re.compile(
r"^\s*-?\s*['\"]?uses['\"]?\s*:\s*['\"]?([^\s#'\"]+)",
re.MULTILINE,
)
secret_pattern = re.compile(
r"\$\{\{\s*secrets\.([A-Za-z0-9_]+)", re.IGNORECASE
)
auth_env_pattern = re.compile(
r"^\s*(NODE_AUTH_TOKEN|NPM_AUTH_TOKEN|NPM_TOKEN|PACKAGES?_TOKEN)"
r"\s*:\s*([^#\r\n]*)",
re.IGNORECASE | re.MULTILINE,
)

for path, text in workflows:
for match in runner_pattern.finditer(text):
value = match.group(1).strip()
if not value or expression_start in value:
add_error(
path,
text,
match.start(),
"runner selection must use an explicit standard GitHub-hosted label",
)
continue

if value.startswith("[") and value.endswith("]"):
labels = [item.strip(" '\"") for item in value[1:-1].split(",")]
else:
labels = [value.strip(" '\"")]

invalid = [label for label in labels if label not in allowed_runners]
if invalid:
add_error(
path,
text,
match.start(),
"runner label is not a free standard GitHub-hosted label: "
+ ", ".join(invalid),
)

for match in event_pattern.finditer(text):
add_error(
path,
text,
match.start(),
"pull_request_target is not allowed for public repository checks",
)

for match in uses_pattern.finditer(text):
target = match.group(1)
if target.casefold().startswith("mnppi/"):
add_error(
path,
text,
match.start(),
"MNPPI actions and reusable workflows are not allowed",
)

for match in secret_pattern.finditer(text):
name = match.group(1).upper()
if "PACKAGE" in name or name in package_secret_names:
add_error(
path,
text,
match.start(),
f"package credential secret is not allowed: {name}",
)

for match in auth_env_pattern.finditer(text):
value = match.group(2).strip().strip("'\"")
if value:
add_error(
path,
text,
match.start(),
f"package credential environment variable must stay unset: {match.group(1)}",
)

private_scope = "@" + "mnppi" + "/"
npm_registry = "npm" + ".pkg." + "github.com"
container_registry = "ghcr" + ".io/" + "mnppi"
mnppi_git = re.compile(r"github\.com[/:]mnppi/", re.IGNORECASE)

for path, text in files:
folded = text.casefold()
for value, message in (
(private_scope, "private MNPPI package scope is not allowed"),
(npm_registry, "GitHub npm registry is not allowed"),
(container_registry, "MNPPI container registry is not allowed"),
):
position = folded.find(value)
if position >= 0:
add_error(path, text, position, message)

if is_dependency_file(path) or any(path == workflow[0] for workflow in workflows):
match = mnppi_git.search(text)
if match:
add_error(
path,
text,
match.start(),
"MNPPI Git dependency is not allowed",
)

if errors:
print("Public repository boundary violations:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
raise SystemExit(1)

print(
f"Public repository boundary passed for {len(workflows)} workflow file(s)."
)
PY

- name: Set up Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
Expand Down
4 changes: 3 additions & 1 deletion profile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ On my own time I love building personal projects on the Cloudflare edge: full-st

## Public Pull Request Checks

GitHub runs the required public plain-language check for each pull request. The check uses a hosted runner and no MNPPI package token.
GitHub runs required security and plain-language checks for each public pull request.
These checks use free standard GitHub-hosted runners.
They do not use private MNPPI packages, registries, reusable workflows, or package credentials.
Loading