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
351 changes: 351 additions & 0 deletions .github/schemas/security-insights-v2.2.0.cue

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .github/workflows/cflite-batch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ permissions: read-all
jobs:
batch-fuzzing:
runs-on: ubuntu-latest
# Runtime cap (same rationale as the dast/codspeed caps, PR #172): the
# fuzz budget is fuzz-seconds=1800 (30 min) + fuzzer build time, so a
# healthy run fits well under 60 minutes — a hung build or a wedged
# fuzzer should not burn the 6-hour default ceiling on a DAILY cron.
timeout-minutes: 60
steps:
- name: Build Fuzzers
id: build
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/verify-changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ on:
- "pyproject.toml"
- "CHANGELOG.md"
- "scripts/extract_changelog_block.py"
- "scripts/check_changelog_vs_commits.py"
- ".github/workflows/verify-changelog.yml"
pull_request:
branches: [main]
paths:
- "pyproject.toml"
- "CHANGELOG.md"
- "scripts/extract_changelog_block.py"
- "scripts/check_changelog_vs_commits.py"
- ".github/workflows/verify-changelog.yml"
# Allow manual re-runs from the Actions UI for diagnostic purposes.
workflow_dispatch:
Expand All @@ -53,6 +55,11 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# Full history + tags for the ADVISORY commits-vs-CHANGELOG diff
# below (it needs the previous release tag as the log baseline).
# The presence gate itself needs no history; the advisory script
# fail-softs to a skip if tags are ever absent.
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
Expand Down Expand Up @@ -110,3 +117,14 @@ jobs:
echo "::notice::CHANGELOG block for [$version] looks healthy ($body_size bytes)."
env:
STEPS_WORKSPACE_VERSION_OUTPUTS_VERSION: ${{ steps.workspace-version.outputs.version }}

# v0.11 H6 (#18): purely ADVISORY diff — at release-prep time (version
# bumped, not yet tagged), list commits since the last release tag
# whose (#NNN) PR ref the new CHANGELOG block never mentions (the
# shape of a shipped-but-undocumented change). ALWAYS exits 0: a
# curated block may fold or omit deliberately (entry-COMPLETE cannot
# be a hard gate without lying); the value is the visible checklist
# in the release-prep PR's CI log. On non-release changes the script
# self-skips (version already tagged).
- name: Advisory — commits since last tag vs the CHANGELOG block
run: python scripts/check_changelog_vs_commits.py
28 changes: 28 additions & 0 deletions .github/workflows/verify-osps-conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,31 @@ jobs:
else
echo "::warning::.local/pre-release-review/osps-conformance.yaml not present (gitignored — won't exist on fresh clones). Skipping schema check."
fi

# v0.11 H6: gate security-insights.yml against the OFFICIAL OpenSSF
# Security Insights v2.2.0 schema, so a manifest edit (or a stale field)
# that stops conforming fails PR CI instead of silently degrading what
# the OSPS Baseline tooling / CLOMonitor / LFX Insights can read. The
# spec publishes CUE, not JSON Schema — the vendored copy at
# .github/schemas/security-insights-v2.2.0.cue is byte-identical to
# spec/schema.cue at ossf/security-insights-spec tag v2.2.0
# (sha256 4e4b61470b5484fc8969f2a8a77ffbe0712a7c8a870c2079188f246c14cd58b1);
# bump the vendored file together with header.schema-version. First
# local vet fired RED on the real manifest (missing required
# security.tools[].rulesets) before the fix — the gate demonstrably
# checks something.
- name: Install cue (v0.17.0, checksum-verified)
run: |
curl -sSfL https://github.com/cue-lang/cue/releases/download/v0.17.0/cue_v0.17.0_linux_amd64.tar.gz \
-o /tmp/cue.tar.gz
echo "e22219a1cb520ab3d660a486ed2222bb7b8e9cb9502a951ce6b8ac668695713f /tmp/cue.tar.gz" | sha256sum -c -
mkdir -p "$HOME/.local/bin"
tar -xzf /tmp/cue.tar.gz -C "$HOME/.local/bin" cue
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Validate security-insights.yml against the Security Insights v2.2.0 schema
run: |
# tool-check: ok cue (installed by the prior curl step "Install cue" in this job)
cue vet -d '#SecurityInsights' \
.github/schemas/security-insights-v2.2.0.cue security-insights.yml
echo "security-insights.yml conforms to Security Insights v2.2.0."
9 changes: 7 additions & 2 deletions docker/Dockerfile.demo
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,13 @@ COPY docker/demo/launch.py /opt/evidentia-demo/launch.py
# base-agnostic — the interpreter is Python 3.13 on both the slim and DHI bases,
# and safe_load never touches the libyaml C extension. The runner's scrubbed
# subprocess omits PYTHONPATH, so the real `evidentia` binary uses the base's own
# installed pyyaml, unaffected by this overlay copy.
RUN pip install --no-cache-dir --target /opt/evidentia-demo "pyyaml==6.0.2"
# installed pyyaml, unaffected by this overlay copy. Hash-pinned via the
# committed requirements file (alert #180): --require-hashes makes pip verify
# the downloaded artifact's sha256, so a registry-side substitution of the
# pinned version fails the build (same mechanism as the main Dockerfile).
COPY docker/demo/requirements.txt /tmp/demo-requirements.txt
RUN pip install --no-cache-dir --require-hashes --target /opt/evidentia-demo \
-r /tmp/demo-requirements.txt

# ── final: FROM the (distroless) signed release base — NO shell, no RUN ───────
# BASE is the global ARG declared above the assets stage (a non-resolvable
Expand Down
24 changes: 24 additions & 0 deletions docker/demo/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Hash-pinned requirement for the demo image's vendored pyyaml overlay
# (Dockerfile.demo `assets` stage). Closes the pinned-dependencies gap on
# the demo image's only network install (code-scanning alert #180, demo
# pip install pinned by version but not hash; same mechanism as
# .github/workflows/requirements/verify-osps-conformance.txt and the main
# Dockerfile's pip-compile --generate-hashes closure): with
# `--require-hashes`, pip verifies each downloaded artifact's sha256
# before installation, so a registry-side substitution of the pinned
# version fails the build instead of landing in the image.
#
# Hashes sourced from pypi.org/pypi/pyyaml/6.0.2/json on 2026-07-10.
# Wheels cover the builder stage's python:3.13-slim on linux/amd64 and
# linux/arm64 (Docker Desktop on Apple silicon); the sdist hash is the
# fallback for any other platform.
#
# When bumping the pyyaml version, re-fetch hashes from PyPI:
# curl -s https://pypi.org/pypi/pyyaml/<version>/json | jq -r '.urls[]
# | select(.filename | endswith(".tar.gz") or
# (contains("cp313") and contains("manylinux_2_17"))) |
# "\(.filename) \(.digests.sha256)"'
pyyaml==6.0.2 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e
169 changes: 169 additions & 0 deletions scripts/check_changelog_vs_commits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Advisory diff: commit subjects since the last tag vs the CHANGELOG block (#18).

The changelog-presence gate (verify-changelog.yml / release.yml) proves a
``## [X.Y.Z]`` block EXISTS and is non-trivial; nothing checks it is
COMPLETE. This advisory closes that narrow gap at release-prep time: when
the workspace version X.Y.Z is bumped but not yet tagged, it lists every
commit landed since the previous release tag whose PR number (the
``(#NNN)`` suffix every merge-queue squash subject carries) is not
mentioned anywhere in the ``[X.Y.Z]`` CHANGELOG block — the shape of a
shipped-but-undocumented change.

PURELY ADVISORY (elite-practice scan #18): always exits 0 on findings —
a hand-curated CHANGELOG legitimately folds several PRs into one bullet
or omits mechanical ones (Dependabot bumps), so a hard gate here would
either lie or nag. The value is the visible checklist in the release-prep
PR's CI log. Deliberately NOT a Conventional-Commits/git-cliff adoption:
release orchestration stays with the atomic tag-driven design.

Fail-soft (lesson 2): no tags visible (shallow clone), no git, an
already-tagged version (nothing being prepped), or a missing block (the
presence gate's job) all mean "nothing to compare" — exit 0 with a note.
Exit 1 only on internal errors.
"""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
from pathlib import Path

try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - py<3.11 unsupported anyway
sys.exit("python >= 3.11 required (tomllib)")

REPO_ROOT = Path(__file__).resolve().parents[1]

_PR_REF_RE = re.compile(r"\(#(\d+)\)")
_BLOCK_HEADING_RE = "## ["


def workspace_version(pyproject_path: Path) -> str | None:
"""The root [project] version — what the next tag will publish."""
try:
data = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
version = data["project"]["version"]
except (OSError, tomllib.TOMLDecodeError, KeyError):
return None
return version if isinstance(version, str) else None


def changelog_block(changelog_text: str, version: str) -> str | None:
"""The ``## [version]`` block's text (to the next ``## [`` heading)."""
heading = f"## [{version}]"
start = changelog_text.find(heading)
if start == -1:
return None
end = changelog_text.find(_BLOCK_HEADING_RE, start + len(heading))
return changelog_text[start : end if end != -1 else len(changelog_text)]


def pr_number(subject: str) -> str | None:
"""The trailing ``(#NNN)`` PR ref of a squash-merge subject, if any."""
matches = _PR_REF_RE.findall(subject)
return matches[-1] if matches else None


def compare(subjects: list[str], block: str) -> tuple[list[str], list[str]]:
"""Return (subjects whose PR ref is absent from the block,
subjects carrying no PR ref at all)."""
missing: list[str] = []
unreferenced: list[str] = []
for subject in subjects:
pr = pr_number(subject)
if pr is None:
unreferenced.append(subject)
elif f"#{pr}" not in block:
missing.append(subject)
return missing, unreferenced


def _git(*args: str) -> str | None:
"""Run git in the repo root; None on any failure (fail-soft)."""
try:
proc = subprocess.run(
["git", *args],
cwd=REPO_ROOT,
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
check=False,
)
except (OSError, subprocess.SubprocessError):
return None
if proc.returncode != 0:
return None
return proc.stdout


def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.parse_args(argv)

version = workspace_version(REPO_ROOT / "pyproject.toml")
if version is None:
print("WARN: could not read the workspace version — skipping", file=sys.stderr)
return 0

if (_git("tag", "-l", f"v{version}") or "").strip():
print(
f"check_changelog_vs_commits: v{version} is already tagged — "
"no release being prepped, nothing to compare."
)
return 0

base = (_git("describe", "--tags", "--abbrev=0", "HEAD") or "").strip()
if not base:
print(
"WARN: no reachable release tag (shallow clone or no tags) — "
"skipping the advisory diff",
file=sys.stderr,
)
return 0

log = _git("log", "--format=%s", f"{base}..HEAD")
if log is None:
print(f"WARN: git log {base}..HEAD failed — skipping", file=sys.stderr)
return 0
subjects = [s for s in log.splitlines() if s.strip()]

try:
changelog_text = (REPO_ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
except OSError as exc:
print(f"WARN: cannot read CHANGELOG.md ({exc}) — skipping", file=sys.stderr)
return 0
block = changelog_block(changelog_text, version)
if block is None:
print(
f"check_changelog_vs_commits: no [{version}] block yet — the "
"changelog-presence gate owns that failure; nothing to compare."
)
return 0

missing, unreferenced = compare(subjects, block)
print(
f"check_changelog_vs_commits: {len(subjects)} commit(s) in "
f"{base}..HEAD vs the [{version}] block:"
)
for subject in missing:
print(f" ADVISORY: PR not mentioned in [{version}]: {subject}")
for subject in unreferenced:
print(f" note (no PR ref, direct commit?): {subject}")
if not missing and not unreferenced:
print(" every commit's PR ref appears in the block.")
else:
print(
f" {len(missing)} PR(s) unmentioned, {len(unreferenced)} "
"subject(s) without a PR ref. ADVISORY ONLY — a curated block "
"may fold or omit deliberately; review, don't chase zero."
)
return 0


if __name__ == "__main__":
sys.exit(main())
8 changes: 6 additions & 2 deletions security-insights.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
header:
schema-version: 2.2.0
last-updated: '2026-06-26'
last-reviewed: '2026-06-26'
last-updated: '2026-07-10'
last-reviewed: '2026-07-10'
url: https://github.com/Polycentric-Labs/evidentia/raw/main/security-insights.yml
comment: |
Single-repository OpenSSF Security Insights manifest for Evidentia, an
Expand Down Expand Up @@ -83,6 +83,8 @@ repository:
tools:
- name: osv-scanner
type: SCA
rulesets:
- osv-scanner.toml (repo-root allowlist; every IgnoredVulns entry carries a security-review-traceable reason + ignoreUntil expiry)
integration:
adhoc: false
ci: true
Expand All @@ -91,6 +93,8 @@ repository:
SBOM-based SCA gate run in CI and at release time (release.yml).
- name: Dependabot
type: SCA
rulesets:
- .github/dependabot.yml (grouped updates, cooldown, python-framework isolation)
integration:
adhoc: false
ci: true
Expand Down
Loading
Loading