feat(audit): close audit-blindness gap for local .apm/ content (#887)#889
Open
danielmeppiel wants to merge 7 commits intomainfrom
Open
feat(audit): close audit-blindness gap for local .apm/ content (#887)#889danielmeppiel wants to merge 7 commits intomainfrom
danielmeppiel wants to merge 7 commits intomainfrom
Conversation
Implement self-entry virtual lockfile mechanism + includes manifest field + hash drift detection + policy gate, addressing the audit-blindness crisis identified by the CISO + crisis panel. ## Wave 1 (foundation) - Lockfile self-entry: synthesize LockedDependency for local content (repo_url=<self>, source=local, local_path=., is_dev=true) at read boundary; pop+restore in to_yaml() with try/finally for byte-stable round-trip. - New get_package_dependencies() excludes self-entry for callers doing dependency-walk operations. - includes manifest field (None | 'auto' | List[str]) with parser. - Policy schema: require_explicit_includes (bool, default false). - Packer guard: skip source==local deps in apm bundles. ## Wave 2 (audit + policy + caller migrations) - _check_lockfile_exists: probes local_deployed_files so local-only repos don't fail. - _check_no_orphans: filters _SELF_KEY. - _check_content_integrity: re-reads files, compares SHA-256 against deployed_file_hashes; skips missing/symlinks/unhashed entries. - New _check_includes_consent: advisory [!] when includes undeclared but local content present (always passes). - New _check_includes_explicit in policy seam, threaded via conditional kwarg from policy_gate. - Migrated 6 callers from get_all_dependencies() to get_package_dependencies() to hide self-entry from walk-and-act operations. 47 new tests added across 5 files. 666 targeted tests pass. Refs: #887 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-trip (#887) Wave 3: verification tests + N1 fix. ## Scanner coverage (compute_deployed_hashes) 9 tests pinning the files-vs-hashes contract: - Every regular file has a corresponding hash entry. - Directories excluded; symlinks excluded (deterministic). - Empty files hashed (well-known SHA-256 of b''). - Hidden files (.mcp.json) hashed. - The set difference 'local_deployed_files - hashes.keys()' equals exactly the directory entries. Live confirmation on this repo: 26 files, 18 hashes, 8 directory diff (under .github/skills/) -- no regular file is missing a hash. ## Packer regression + N1 fix Architect's N1 finding: enrich_lockfile_for_pack() was serializing 'local_deployed_files' / 'local_deployed_file_hashes' verbatim into bundle lockfiles, causing a phantom self-entry on unpack whose files the unpacker would then fail to verify. Fix at bundle/lockfile_enrichment.py: strip both fields from the bundle lockfile dict after YAML round-trip and before pack: metadata serialization. Original LockFile object untouched. 4 regression tests + live 'apm pack' confirms bundle lockfile is clean. ## Integration round-trip (5 tests, all pass live) A. Install records self-entry (local_deployed_files + hashes). B. Audit clean install: content-integrity passes, includes-consent emits [!] advisory. C. Audit detects drift: tampered file -> exit 1 + 'hash-drift: <path>'. D. Includes declared (auto or list): no [!] advisory. E. Policy require_explicit_includes: blocks undeclared/auto. End-to-end via subprocess (matches existing test_local_install.py convention). 65 targeted tests pass. Refs: #887 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
) Wave 4 + critical fixes for the local-content audit work: - apm.yml: declare 'includes: auto' to consent to local .apm/ deployment governance for this repo's own dogfood. - pyproject.toml + uv.lock: bump to 0.10.0 (target release for #887). - CHANGELOG.md: Added/Fixed/Changed entries under [Unreleased]. - Docs: lockfile-spec.md ($4.5 Self-Entry Convention), manifest-schema.md ($3.9 'includes' field), governance-guide.md (remove drift-gap caveats now that hash verification ships), apm-usage/governance.md skill. Critical bug fixes discovered during dogfood: - cleanup.py: orphan loop iterates lockfile.dependencies which now includes the synthesized self-entry under '.'. Without a guard, every install was deleting all 26 deployed local files because '.' is never in apm.yml's external dependencies. Add explicit skip on _SELF_KEY before the orphan check. - lockfile.py: is_semantically_equivalent compared local_deployed_files but skipped local_deployed_file_hashes. Hash-only changes were treated as 'no change', so post-install hash refreshes never persisted, causing audit to report stale hash drift forever. Add the hash-dict comparison. Verified locally: install clean, audit 7/7 green, drift introduced detected, drift removed back to green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three-panel review (security, devx, logging) feedback applied:
Security:
- ci_checks.py:_check_content_integrity: gate every hash-verification
read through BaseIntegrator.validate_deploy_path so a forged lockfile
with a traversal path (e.g. '../../.env') cannot induce reads outside
managed locations or leak hashes via audit output.
DevX UX:
- _helpers.py:_create_minimal_apm_yml: scaffold 'includes: auto' so
'apm init' projects don't trip the includes-consent advisory the
moment a primitive lands in .apm/. Matches what manifest-schema.md
promises ('default for newly initialised projects').
- policy_checks.py:_check_includes_explicit: branch the error message
-- 'add includes: [...]' when manifest has no includes field, and
'replace includes: auto' only when the user actually wrote auto.
- ci_checks.py: render the synthesized self-entry as 'dep=<self>'
rather than the internal '.' constant in hash-drift details.
- Docs: bump check counts (6 baseline -> 7, 16 policy -> 17) across
governance-guide.md, apm-policy.md, ci-cd.md, ci-policy-setup.md.
Add includes-consent to enumerated baseline list. Document hash
drift detection in cli-commands.md ('What it detects').
- manifest-schema.md: correct 'rejected at parse time' to 'rejected
at install/audit time by the explicit-includes policy check'.
Logging:
- ci_checks.py:_check_content_integrity: build remediation message
conditionally so hash-only failures don't suggest 'apm audit --strip'
(which only strips Unicode).
- ci_checks.py: truncate hashes in hash-drift detail line for terminal
width (full hashes still in JSON output).
- ci_checks.py:_check_includes_consent: drop '[!]' prefix from
message text -- the audit table renderer owns the status column;
embedding [!] inside a passed=True row produced contradictory
'[+] ... [!] ...' output. Replace 'consent check N/A' with
'includes consent check skipped' (no jargon).
- test_file_scanner.py: replace section-sign character with ASCII.
All 5383 unit tests pass (1 pre-existing flaky MCP scope test
unrelated). Live verification: install clean, audit 7/7 green,
drift detection works, 'apm init -y' scaffolds 'includes: auto'.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Closes the apm audit --ci integrity gap for locally-deployed .apm/ content by projecting local lockfile state into a virtual self-entry and extending baseline audit to hash-verify all managed files (including local ones), plus adding an includes: manifest consent signal with optional policy enforcement.
Changes:
- Synthesize a virtual
./<self>lockfile dependency in-memory and update key call sites to skip it where appropriate. - Extend baseline
content-integrityto include per-file SHA-256 drift detection; addincludes-consentbaseline advisory andmanifest.require_explicit_includespolicy check. - Add/adjust tests and docs to cover the new audit behavior, policy seam wiring, and bundling safeguards (strip local-content metadata; skip local-source deps in bundles).
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Bumps apm-cli version to 0.10.0. |
| pyproject.toml | Bumps project version to 0.10.0. |
| apm.yml | Bumps repo manifest version and adds includes: auto. |
| apm.lock.yaml | Updates local deployed file hashes (dogfood drift baseline). |
| CHANGELOG.md | Adds Unreleased entries for self-entry/includes/audit hash drift and related fixes. |
| docs/src/content/docs/reference/manifest-schema.md | Documents new includes: manifest field and policy interaction. |
| docs/src/content/docs/reference/lockfile-spec.md | Documents the in-memory self-entry convention and invariants. |
| docs/src/content/docs/reference/cli-commands.md | Updates apm audit --ci baseline check enumeration and hash drift behavior. |
| docs/src/content/docs/integrations/ci-cd.md | Updates baseline/policy check counts (7 + 17). |
| docs/src/content/docs/guides/ci-policy-setup.md | Updates baseline/policy check counts and text references. |
| docs/src/content/docs/enterprise/governance-guide.md | Updates governance matrix/counts and adds explicit-includes policy documentation. |
| docs/src/content/docs/enterprise/apm-policy.md | Updates CI-time baseline check count in docs. |
| packages/apm-guide/.apm/skills/apm-usage/governance.md | Extends governance skill docs with explicit-includes knob + includes: explanation. |
| src/apm_cli/deps/lockfile.py | Adds _SELF_KEY, synthesizes self-entry in from_yaml(), excludes it in new get_package_dependencies(), updates equivalence and installed-paths guard. |
| src/apm_cli/policy/ci_checks.py | Extends baseline checks: local-only lockfile relevance, orphan skip for self, hash drift verification, adds includes-consent advisory. |
| src/apm_cli/policy/policy_checks.py | Adds explicit-includes policy check and a seam param to run it only when manifest context is provided. |
| src/apm_cli/policy/parser.py | Parses/validates manifest.require_explicit_includes. |
| src/apm_cli/policy/schema.py | Adds require_explicit_includes to policy schema. |
| src/apm_cli/install/phases/policy_gate.py | Threads ctx.apm_package.includes into dependency policy checks when available. |
| src/apm_cli/install/phases/cleanup.py | Protects the synthesized self-entry from orphan cleanup deletion. |
| src/apm_cli/bundle/packer.py | Skips source=local dependencies when collecting deployed files for apm bundles. |
| src/apm_cli/bundle/lockfile_enrichment.py | Strips local_deployed_files + local_deployed_file_hashes from bundle lockfile YAML. |
| src/apm_cli/models/apm_package.py | Adds includes field parsing/validation to APMPackage. |
| src/apm_cli/integration/skill_integrator.py | Uses get_package_dependencies() to avoid <self> in ownership maps. |
| src/apm_cli/integration/mcp_integrator.py | Uses get_package_dependencies() to avoid <self> during transitive collection. |
| src/apm_cli/commands/_helpers.py | apm init scaffolds includes: auto and skips self-entry in install-path expectations. |
| src/apm_cli/commands/uninstall/engine.py | Uses get_package_dependencies() to avoid self-entry crashes in uninstall flows. |
| src/apm_cli/commands/deps/cli.py | Uses get_package_dependencies() so self-entry does not show up in apm deps tree. |
| tests/unit/test_lockfile_self_entry.py | Adds unit coverage for self-entry synthesis + serialization invariants. |
| tests/unit/test_self_entry_caller_guards.py | Regression tests for representative callers skipping the self-entry. |
| tests/unit/test_packer.py | Adds tests for excluding local-source deps in bundles and stripping local-content fields. |
| tests/unit/test_deps_list_tree_info.py | Adds guard test ensuring deps tree hides self-entry. |
| tests/unit/test_ci_checks.py | Adds baseline audit coverage for hash drift + local-only repo behavior + includes-consent. |
| tests/unit/test_audit_policy_command.py | Updates baseline check count expectations in CI/policy JSON tests. |
| tests/unit/test_apm_package.py | Adds unit tests for includes parsing and behavior. |
| tests/unit/install/test_policy_gate_phase.py | Tests that policy_gate threads manifest_includes correctly. |
| tests/unit/policy/test_policy_checks.py | Updates expected policy check count to 17 and imports explicit-includes check. |
| tests/unit/policy/test_run_dependency_policy_checks.py | Adds seam-level tests for explicit-includes check gating. |
| tests/unit/policy/test_parser.py | Adds parser/validator tests for require_explicit_includes. |
| tests/unit/install/test_file_scanner.py | Adds coverage tests for compute_deployed_hashes directory/symlink/empty-file behavior. |
| tests/integration/test_local_content_audit.py | Adds end-to-end subprocess tests for local-content install + audit drift detection + policy enforcement. |
Copilot's findings
Comments suppressed due to low confidence (1)
docs/src/content/docs/reference/manifest-schema.md:180
- This section claims
apm auditemits an[!]includes-consent advisory, but the current CI renderer only shows pass/fail status symbols andincludes-consentreturnspassed=Truewith no[!]prefix in the message. Either update the implementation to include[!]in theincludes-consentmessage (including JSON/SARIF), or adjust this doc text to describe the advisory without asserting a specific status symbol.
Declares which local `.apm/` content the project consents to publish when packing or deploying. Three forms are supported:
1. **Undeclared** -- field omitted. Legacy behaviour: all local `.apm/` content is published as if `auto` were set. `apm audit` emits an `[!]` includes-consent advisory whenever local content is deployed under this form.
2. **`includes: auto`** -- explicit consent to publish all local `.apm/` content via the file scanner. No path enumeration required. Default for newly initialised projects.
- Files reviewed: 40/41 changed files
- Comments generated: 5
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/apm/sessions/fb9d2cd4-ba19-4ecb-b6cd-743cd5743bdd Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
This was referenced Apr 24, 2026
Collaborator
Author
Note on follow-upThis PR is the foundation that Epic #898 builds on. The Epic-PR will rebase on top of the work landed here and extend it with:
No scope change requested here; this PR ships as designed and closes #684 + #887. The Epic owns the next layer. |
# Conflicts: # CHANGELOG.md # apm.lock.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & Why
CISOs reported an audit-blindness gap:
apm installwas deploying local.apm/content into governed directories (.github/,.claude/, etc.), butapm audit --cinever hash-verified those deployed files. The lockfile recordedlocal_deployed_filesand their hashes, yet the audit pipeline only walked package dependencies — so post-install tampering of any locally-sourced file went undetected.This PR closes the gap with end-to-end hash verification of every managed file plus a new
includes:manifest field that gives users (and enterprises) explicit governance consent over what local content gets deployed.Closes #887.
Solution overview
<self>dependency keyed by_SELF_KEY = ".". The on-disk YAML schema is unchanged;to_yaml()does a round-trip pop+restore so the synthetic entry never persists.includes:manifest field — three modes:auto(deploy all.apm/), an explicit list, or omitted (deploy nothing). Purely additive; existing manifests keep working.content-integrityaudit check — now covers the self-entry alongside package deps, detecting post-install tampering on any managed file.includes-consent— advisory only, never blocks.manifest.require_explicit_includes(block/warn/off) — enterprises can mandate explicit include lists.apm init -yscaffoldsincludes: auto— clean first-run UX.Critical bug fixes discovered during dogfood
These were caught while dogfooding the new audit on the APM repo itself and are essential to the fix:
cleanup.py— orphan loop was deleting all 26 deployed files post-install because the synthesized self-entry was misclassified as an orphan package. Fixed by treating_SELF_KEYas protected.lockfile.py—is_semantically_equivalentskippedlocal_deployed_file_hashescomparison, so hash-only updates never persisted. Fixed by including the hashes in the equivalence check.lockfile_enrichment.py(bundle path) — local-content fields needed stripping after the YAML round-trip. Fixed.Audit coverage: before / after
flowchart LR subgraph BEFORE["BEFORE (audit blind to local .apm/)"] I1[apm install] --> S1[.apm/ source files] S1 --> D1[.github/ deployed] I1 --> L1[apm.lock.yaml<br/>local_deployed_files + hashes<br/>written but never read] A1[apm audit --ci] --> L1 A1 -.never reads.-> D1 D1 -.HASH DRIFT INVISIBLE.-> X1((CISO blind)) endflowchart LR subgraph AFTER["AFTER (every managed file verified)"] I2[apm install] --> S2[.apm/ source files] S2 --> D2[.github/ deployed] I2 --> L2[apm.lock.yaml<br/>local_deployed_files + hashes] L2 --> SE[from_yaml synthesizes<br/>self-entry under <self>] A2[apm audit --ci] --> L2 SE --> HV[content-integrity:<br/>hash-verify every<br/>deployed file] HV -. detects .-> D2 HV --> OK([7/7 green]) HV --> DRIFT([hash-drift: file<br/>dep=<self>]) endVerification
5383/5384pass. The sole failure is the pre-existing flakytest_user_scope_skips_workspace_runtimes, unrelated to this change.apm installclean,apm audit --cireturns 7/7 greendep=<self>apm init -yconfirmed to scaffoldincludes: auto.auth-expert.agent.mdandcicd.instructions.mdremains visible — these are real post-install hand-edits from prior PRs and are handled outside this PR.Panel reviews
All three specialist panels reviewed; CEO signed off SHIP.
initscaffold, doc check counts updated (6+16 -> 7+17), error wording branched,dep=<self>label introduced,cli-commands.mdupdated.Breaking changes
None. Purely additive. No migration required for any user class — existing manifests, lockfiles, and CI flows continue to work unchanged.
Version
Bumps to 0.10.0 (minor: significant new functionality, no breakage).
Test plan
uv run pytest tests/unit tests/test_console.py uv run pytest tests/integration/test_local_content_audit.py # new file: 5 subprocess tests A-E