Skip to content
Open
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
32 changes: 31 additions & 1 deletion .github/workflows/secret-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,34 @@ jobs:
# --log-opts=HEAD scans the full history of the checked-out commit (the
# PR merge commit, or main). Without it gitleaks scans every fetched
# branch, so one branch's finding fails every other pull request.
run: gitleaks git . --config .gitleaks.toml --redact --no-banner --log-opts=HEAD
run: |
set -euo pipefail
report_dir="$(mktemp -d "${RUNNER_TEMP}/gitleaks-report.XXXXXX")"
trap 'rm -rf -- "$report_dir"' EXIT
report_path="${report_dir}/metadata.tsv"
template_path="${report_dir}/metadata.tmpl"

# Gitleaks' JSON Finding includes fields beyond redacted Secret,
# Match, and Line (including optional Fragment). Use its supported
# template reporter to serialize only these four metadata fields.
# Base64 avoids control-character delimiters in repository paths.
cat > "$template_path" <<'EOF'
{{range .}}{{printf "%s\t%s\t%d\t%s\n" (.RuleID | b64enc) (.File | b64enc) .StartLine (.Commit | b64enc)}}{{end -}}
EOF

# Never let scanner output enter Actions logs. The template report
# contains only RuleID, repository-relative file/line, and commit.
set +e
gitleaks git . \
--config .gitleaks.toml \
--redact=100 \
--no-banner \
--log-opts=HEAD \
--report-format=template \
--report-template "$template_path" \
--report-path "$report_path" \
>/dev/null 2>&1
scanner_exit_code=$?
set -e

python3 scripts/ci/gitleaks_metadata.py "$report_path" "$scanner_exit_code"
37 changes: 37 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,40 @@ e8ec96725c4451aa64cb97230c18d04b55d98503:packages/flow/tests/e2e/test_mcp_teach_
# container with a fixed synthetic superuser token (a run of hex digits, never a
# credential for any real system); the harness now generates it per run.
9922cee8dbbbaa4494b1c69ec6c71e4899305de6:packages/flow/benchmark/api_generalization/apps/nb.py:generic-api-key:60
# Bundle compatibility fixtures (PR #1928, part of #1899): the generic-api-key rule
# matched `source_hashes` entries in the pinned 2.2.0/2.5.0/2.6.0 provenance.json
# fixtures. Each value is the SHA-256 of that source file at the fixture's pinned
# release commit (verified for all 33). Content digests, not credentials.
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:66
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:182
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:193
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:248
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:290
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:348
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:397
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:416
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:428
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:85
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:155
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:190
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:275
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:351
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:414
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:488
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:508
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:528
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:570
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:643
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:655
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:85
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:132
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:168
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:254
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:316
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:379
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:468
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:490
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:511
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:554
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:630
8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:642
40 changes: 40 additions & 0 deletions packages/flow/docs/BUNDLE_SCHEMA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Bundle schema and sealed identity

`Workflow.load` checks the original bundle's manifest-free canonical payload
before upgrading its schema or constructing the current model. Each supported
schema has one canonical projection. A digest mismatch refuses the bundle.
Schema 2's reviewed default omissions apply only at explicit IR ownership
positions; arbitrary request bodies and mappings remain sealed content.

A missing schema version means legacy schema 1. Upgrades run one registered
step at a time on a copy. Malformed versions, future versions and missing
upgrade steps refuse loading. A breaking format change requires a schema bump,
a canonical format rule and an explicit upgrader.

Loading preserves the original files and content identity. The loaded object
retains the original canonical bytes plus a fingerprint of the upgraded model
and its integrity-manifest fields. Later verification checks both representations
and the sealed assets. Changing the workflow, asset hashes, integrity manifest
or asset bytes requires a new seal. Loading with `verify_integrity=False` does
not make a broken original seal pass a later integrity check.

Admission and authorization use `content_identity_digest` to retain a verified
original identity across model upgrades. It checks the loaded model, original
payload, integrity manifest and supplied asset hashes before returning that
identity. Callers checking live assets must hash their bytes or call
`verify_integrity` as well.

`Workflow.save(..., seal_manifest=True)` creates a seal over the current
serialization; this is also the default save behavior. Saving a loaded sealed
object with `seal_manifest=False` refuses. A format upgrade can change the new
seal's identity, so existing admissions must be evaluated against the new seal.
`compute_content_digest` continues to calculate current-format content for new
seals.

The frozen compatibility fixtures cover synthetic linear and program bundles
produced and self-verified by the pinned 2.2.0, 2.5.0 and 2.6.0 source releases.
Their original bytes and provenance were frozen before testing the current
loader. The fixture README records the historical environment limitations.
Current controls verify loading, integrity, tamper refusal and one exact input
in an owned synthetic headless Chromium page. Content digests provide integrity;
run authorization still applies its policy, identity and effect checks.
4 changes: 2 additions & 2 deletions packages/flow/openadapt_flow/admit_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,15 @@ def _now() -> str:
def _bundle_digest(bundle: Path) -> str:
from openadapt_flow import sealed_set
from openadapt_flow.bundle_validation import (
compute_content_digest,
compute_file_hashes,
content_identity_digest,
)
from openadapt_flow.ir import Workflow

workflow = Workflow.load(bundle)
# A sealed bundle's crops are ciphertext on disk; hash the plaintext.
assets = workflow.decrypted_templates() if sealed_set.is_sealed(bundle) else None
return compute_content_digest(
return content_identity_digest(
workflow, compute_file_hashes(workflow, bundle, assets=assets)
)

Expand Down
4 changes: 2 additions & 2 deletions packages/flow/openadapt_flow/admitted_composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,16 +401,16 @@ def live_bundle_content_digest(workflow: Workflow, bundle_dir: Path | str) -> st
"""SHA-256 of the live admitted bundle, independent of the envelope."""

from openadapt_flow.bundle_validation import (
compute_content_digest,
compute_file_hashes,
content_identity_digest,
)

bundle = Path(bundle_dir)
if workflow.manifest is not None and workflow.manifest.file_hashes:
file_hashes = dict(workflow.manifest.file_hashes)
else:
file_hashes = compute_file_hashes(workflow, bundle)
return compute_content_digest(workflow, file_hashes)
return content_identity_digest(workflow, file_hashes)


def load_child_envelope(
Expand Down
10 changes: 4 additions & 6 deletions packages/flow/openadapt_flow/authorization_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,15 @@ def __init__(self, code: str, detail: str = "") -> None:


def bundle_digest(bundle_dir: Path) -> str:
"""Recompute a bundle's content digest from its files (not from its manifest)."""
"""Verify content identity against the original seal and current asset bytes."""
from openadapt_flow.bundle_validation import (
compute_content_digest,
compute_file_hashes,
content_identity_digest,
)
from openadapt_flow.ir import Workflow

workflow = Workflow.model_validate_json(
(Path(bundle_dir) / "workflow.json").read_text(encoding="utf-8")
)
return compute_content_digest(workflow, compute_file_hashes(workflow, bundle_dir))
workflow = Workflow.load(bundle_dir)
return content_identity_digest(workflow, compute_file_hashes(workflow, bundle_dir))


def matching_bundle(digest: str, candidates: Sequence[Path]) -> Optional[Path]:
Expand Down
Loading
Loading