mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-10 22:18:59 +00:00
296 lines
12 KiB
Python
296 lines
12 KiB
Python
"""Trusted, exact-match waivers for the public-skill CI review gate."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from datetime import date
|
|
from pathlib import Path, PurePosixPath
|
|
from typing import Any
|
|
|
|
SCHEMA_VERSION = "deerflow.skill-review-waivers.v1"
|
|
MANIFEST_PATH = PurePosixPath(".github/skill-review-waivers.v1.json")
|
|
_SHA256_RE = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
_SAFE_REF_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]*\Z")
|
|
_REQUIRED_ENTRY_FIELDS = {
|
|
"package",
|
|
"source",
|
|
"rule_id",
|
|
"path",
|
|
"line",
|
|
"evidence",
|
|
"file_sha256",
|
|
"reason",
|
|
"expires_on",
|
|
}
|
|
_OPTIONAL_ENTRY_FIELDS = {"approved_in", "preapproved_file_sha256s"}
|
|
_MAX_PREAPPROVED_FILE_SHA256S = 8
|
|
|
|
|
|
class WaiverManifestError(ValueError):
|
|
"""Raised when a waiver manifest is missing required trust properties."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SkillReviewWaiver:
|
|
package: str
|
|
source: str
|
|
rule_id: str
|
|
path: str
|
|
line: int
|
|
evidence: str
|
|
file_sha256: str
|
|
reason: str
|
|
expires_on: date
|
|
approved_in: str | None = None
|
|
preapproved_file_sha256s: tuple[str, ...] = ()
|
|
|
|
@property
|
|
def finding_key(self) -> tuple[str, str, str, int, str]:
|
|
return (self.source, self.rule_id, self.path, self.line, self.evidence)
|
|
|
|
@property
|
|
def approved_file_sha256s(self) -> tuple[str, ...]:
|
|
return (self.file_sha256, *self.preapproved_file_sha256s)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WaiverManifest:
|
|
waivers: tuple[SkillReviewWaiver, ...] = ()
|
|
|
|
|
|
EMPTY_MANIFEST = WaiverManifest()
|
|
|
|
|
|
def parse_manifest(payload: bytes | str, *, source: str) -> WaiverManifest:
|
|
"""Parse the versioned waiver manifest with strict, fail-closed validation."""
|
|
try:
|
|
data = json.loads(payload)
|
|
except (json.JSONDecodeError, UnicodeDecodeError, TypeError) as exc:
|
|
raise WaiverManifestError(f"{source}: invalid JSON: {exc}") from exc
|
|
|
|
if not isinstance(data, dict):
|
|
raise WaiverManifestError(f"{source}: manifest root must be an object")
|
|
unknown_root = set(data) - {"schema_version", "waivers"}
|
|
if unknown_root:
|
|
raise WaiverManifestError(f"{source}: unknown root field(s): {', '.join(sorted(unknown_root))}")
|
|
if data.get("schema_version") != SCHEMA_VERSION:
|
|
raise WaiverManifestError(f"{source}: schema_version must be {SCHEMA_VERSION!r}")
|
|
raw_waivers = data.get("waivers")
|
|
if not isinstance(raw_waivers, list):
|
|
raise WaiverManifestError(f"{source}: waivers must be an array")
|
|
if len(raw_waivers) > 256:
|
|
raise WaiverManifestError(f"{source}: waivers must contain at most 256 entries")
|
|
|
|
waivers: list[SkillReviewWaiver] = []
|
|
identities: set[tuple[str, str, str, int, str, str]] = set()
|
|
for index, raw in enumerate(raw_waivers):
|
|
entry_source = f"{source}: waivers[{index}]"
|
|
if not isinstance(raw, dict):
|
|
raise WaiverManifestError(f"{entry_source}: entry must be an object")
|
|
fields = set(raw)
|
|
missing = _REQUIRED_ENTRY_FIELDS - fields
|
|
unknown = fields - _REQUIRED_ENTRY_FIELDS - _OPTIONAL_ENTRY_FIELDS
|
|
if missing:
|
|
raise WaiverManifestError(f"{entry_source}: missing field(s): {', '.join(sorted(missing))}")
|
|
if unknown:
|
|
raise WaiverManifestError(f"{entry_source}: unknown field(s): {', '.join(sorted(unknown))}")
|
|
|
|
package = _canonical_relative_path(raw["package"], field=f"{entry_source}.package")
|
|
if not package.startswith("skills/public/"):
|
|
raise WaiverManifestError(f"{entry_source}.package: must be below skills/public/")
|
|
path = _canonical_relative_path(raw["path"], field=f"{entry_source}.path")
|
|
source_name = _nonempty_string(raw["source"], field=f"{entry_source}.source")
|
|
rule_id = _nonempty_string(raw["rule_id"], field=f"{entry_source}.rule_id")
|
|
evidence = _nonempty_string(raw["evidence"], field=f"{entry_source}.evidence")
|
|
file_sha256 = _parse_sha256(raw["file_sha256"], field=f"{entry_source}.file_sha256")
|
|
raw_preapproved_hashes = raw.get("preapproved_file_sha256s", [])
|
|
if not isinstance(raw_preapproved_hashes, list):
|
|
raise WaiverManifestError(f"{entry_source}.preapproved_file_sha256s: must be an array")
|
|
if len(raw_preapproved_hashes) > _MAX_PREAPPROVED_FILE_SHA256S:
|
|
raise WaiverManifestError(f"{entry_source}.preapproved_file_sha256s: must contain at most {_MAX_PREAPPROVED_FILE_SHA256S} entries")
|
|
preapproved_file_sha256s = tuple(_parse_sha256(value, field=f"{entry_source}.preapproved_file_sha256s[{hash_index}]") for hash_index, value in enumerate(raw_preapproved_hashes))
|
|
if len(set(preapproved_file_sha256s)) != len(preapproved_file_sha256s):
|
|
raise WaiverManifestError(f"{entry_source}.preapproved_file_sha256s: entries must be unique")
|
|
if file_sha256 in preapproved_file_sha256s:
|
|
raise WaiverManifestError(f"{entry_source}.preapproved_file_sha256s: must not repeat file_sha256")
|
|
reason = _nonempty_string(raw["reason"], field=f"{entry_source}.reason")
|
|
if len(reason) < 20:
|
|
raise WaiverManifestError(f"{entry_source}.reason: must contain at least 20 characters")
|
|
line = raw["line"]
|
|
if isinstance(line, bool) or not isinstance(line, int) or line < 1:
|
|
raise WaiverManifestError(f"{entry_source}.line: must be a positive integer")
|
|
expires_on = _parse_date(raw["expires_on"], field=f"{entry_source}.expires_on")
|
|
approved_in = raw.get("approved_in")
|
|
if approved_in is not None:
|
|
approved_in = _nonempty_string(approved_in, field=f"{entry_source}.approved_in")
|
|
|
|
waiver = SkillReviewWaiver(
|
|
package=package,
|
|
source=source_name,
|
|
rule_id=rule_id,
|
|
path=path,
|
|
line=line,
|
|
evidence=evidence,
|
|
file_sha256=file_sha256,
|
|
reason=reason,
|
|
expires_on=expires_on,
|
|
approved_in=approved_in,
|
|
preapproved_file_sha256s=preapproved_file_sha256s,
|
|
)
|
|
identity = (*waiver.finding_key, waiver.package)
|
|
if identity in identities:
|
|
raise WaiverManifestError(f"{entry_source}: duplicates an earlier waiver")
|
|
identities.add(identity)
|
|
waivers.append(waiver)
|
|
|
|
return WaiverManifest(tuple(waivers))
|
|
|
|
|
|
def load_manifest_at_ref(repo_root: Path, ref: str, *, label: str) -> WaiverManifest:
|
|
"""Read a manifest from a Git ref; absence means no waivers at that ref."""
|
|
if not _SAFE_REF_RE.fullmatch(ref) or ref.startswith("-") or ".." in ref or ":" in ref:
|
|
raise WaiverManifestError(f"{label}: unsafe Git ref {ref!r}")
|
|
object_name = f"{ref}:{MANIFEST_PATH.as_posix()}"
|
|
result = subprocess.run(
|
|
["git", "show", object_name],
|
|
cwd=repo_root,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
return EMPTY_MANIFEST
|
|
return parse_manifest(result.stdout, source=f"{label} manifest at {ref}")
|
|
|
|
|
|
def file_sha256(repo_root: Path, waiver: SkillReviewWaiver) -> str | None:
|
|
resolved_repo_root = repo_root.resolve()
|
|
package_candidate = resolved_repo_root / waiver.package
|
|
if package_candidate.is_symlink() or not package_candidate.is_dir():
|
|
return None
|
|
package_root = package_candidate.resolve()
|
|
try:
|
|
package_root.relative_to(resolved_repo_root / "skills" / "public")
|
|
except ValueError:
|
|
return None
|
|
candidate = package_root / waiver.path
|
|
if candidate.is_symlink() or not candidate.is_file():
|
|
return None
|
|
resolved = candidate.resolve()
|
|
try:
|
|
resolved.relative_to(package_root)
|
|
except ValueError:
|
|
return None
|
|
digest = hashlib.sha256(resolved.read_bytes()).hexdigest()
|
|
return f"sha256:{digest}"
|
|
|
|
|
|
def finding_key(finding: dict[str, Any]) -> tuple[str, str, str, int, str] | None:
|
|
source = finding.get("source")
|
|
rule_id = finding.get("rule_id")
|
|
path = finding.get("path")
|
|
line = finding.get("line")
|
|
evidence = finding.get("evidence")
|
|
if not isinstance(source, str) or not isinstance(rule_id, str) or not isinstance(path, str) or isinstance(line, bool) or not isinstance(line, int) or not isinstance(evidence, str):
|
|
return None
|
|
return (source, rule_id, path, line, evidence)
|
|
|
|
|
|
def matching_waiver(
|
|
finding: dict[str, Any],
|
|
*,
|
|
package: str,
|
|
manifest: WaiverManifest,
|
|
repo_root: Path,
|
|
today: date | None = None,
|
|
) -> SkillReviewWaiver | None:
|
|
"""Return an active waiver only for an exact error finding and file digest."""
|
|
if finding.get("severity") != "error":
|
|
return None
|
|
key = finding_key(finding)
|
|
if key is None:
|
|
return None
|
|
current_date = today or date.today()
|
|
for waiver in manifest.waivers:
|
|
if waiver.package != package or waiver.finding_key != key:
|
|
continue
|
|
if waiver.expires_on < current_date:
|
|
continue
|
|
if file_sha256(repo_root, waiver) not in waiver.approved_file_sha256s:
|
|
continue
|
|
return waiver
|
|
return None
|
|
|
|
|
|
def validate_manifest_against_facts(
|
|
manifest: WaiverManifest,
|
|
*,
|
|
facts_by_package: dict[str, dict[str, Any]],
|
|
repo_root: Path,
|
|
today: date | None = None,
|
|
) -> list[str]:
|
|
"""Reject expired, stale, hash-mismatched, or non-error waiver entries."""
|
|
errors: list[str] = []
|
|
current_date = today or date.today()
|
|
for waiver in manifest.waivers:
|
|
description = f"{waiver.package}/{waiver.path}:{waiver.line} ({waiver.source}/{waiver.rule_id})"
|
|
if waiver.expires_on < current_date:
|
|
errors.append(f"{description}: waiver expired on {waiver.expires_on.isoformat()}")
|
|
continue
|
|
facts = facts_by_package.get(waiver.package)
|
|
if facts is None:
|
|
errors.append(f"{description}: package review facts are unavailable")
|
|
continue
|
|
matches = [finding for finding in facts.get("findings", []) if isinstance(finding, dict) and finding_key(finding) == waiver.finding_key]
|
|
if not matches:
|
|
errors.append(f"{description}: no exact current finding matches this waiver")
|
|
continue
|
|
if len(matches) != 1:
|
|
errors.append(f"{description}: expected one exact current finding, found {len(matches)}")
|
|
continue
|
|
if any(finding.get("severity") != "error" for finding in matches):
|
|
errors.append(f"{description}: waivers may target error findings only; blockers can never be waived")
|
|
continue
|
|
actual_hash = file_sha256(repo_root, waiver)
|
|
if actual_hash not in waiver.approved_file_sha256s:
|
|
expected_hashes = ", ".join(waiver.approved_file_sha256s)
|
|
errors.append(f"{description}: file digest changed (expected one of {expected_hashes}, found {actual_hash or 'unavailable'})")
|
|
return errors
|
|
|
|
|
|
def _canonical_relative_path(value: object, *, field: str) -> str:
|
|
text = _nonempty_string(value, field=field)
|
|
if "\\" in text:
|
|
raise WaiverManifestError(f"{field}: backslashes are not allowed")
|
|
path = PurePosixPath(text)
|
|
if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts) or path.as_posix() != text:
|
|
raise WaiverManifestError(f"{field}: must be a canonical relative POSIX path without traversal")
|
|
return text
|
|
|
|
|
|
def _nonempty_string(value: object, *, field: str) -> str:
|
|
if not isinstance(value, str) or not value.strip() or value != value.strip():
|
|
raise WaiverManifestError(f"{field}: must be a non-empty string without surrounding whitespace")
|
|
return value
|
|
|
|
|
|
def _parse_sha256(value: object, *, field: str) -> str:
|
|
digest = _nonempty_string(value, field=field)
|
|
if not _SHA256_RE.fullmatch(digest):
|
|
raise WaiverManifestError(f"{field}: must be sha256 followed by 64 lowercase hex characters")
|
|
return digest
|
|
|
|
|
|
def _parse_date(value: object, *, field: str) -> date:
|
|
text = _nonempty_string(value, field=field)
|
|
try:
|
|
parsed = date.fromisoformat(text)
|
|
except ValueError as exc:
|
|
raise WaiverManifestError(f"{field}: must be an ISO 8601 calendar date") from exc
|
|
if parsed.isoformat() != text:
|
|
raise WaiverManifestError(f"{field}: must use YYYY-MM-DD format")
|
|
return parsed
|