feat(marketplace): add audit command to flag deps that bypass marketplace pinning#881
feat(marketplace): add audit command to flag deps that bypass marketplace pinning#881edenfunf wants to merge 3 commits intomicrosoft:mainfrom
Conversation
danielmeppiel
left a comment
There was a problem hiding this comment.
Strategic context
Thanks for this work, @edenfunf. A dependency-bypass auditor is exactly the kind of marquee surface APM's Secure-by-default story needs, and the supply-chain rigor in this PR (see "What's already great" below) is well above the bar we usually see for a first contribution. We very much want to land this -- but the branch has drifted hard against main since you opened it, and a clean rebase isn't mechanical anymore. Below are the three blockers, with concrete options for each.
Blockers (ordered)
[x] 1. Naming collision: apm marketplace doctor already exists on main
Since refactor PR #1024, main ships an apm marketplace doctor command at src/apm_cli/commands/marketplace/doctor.py that runs environment diagnostics (git binary, network reachability, auth, marketplace config readiness). Your doctor is a dependency-bypass auditor -- different inputs, different outputs, different mental model. They cannot coexist under the same name.
Resolution options (pick one -- see "Naming proposals" below for our recommendation).
[x] 2. Target file src/apm_cli/commands/marketplace.py was deleted on main
PR #1024 split that single file into a marketplace/ package: __init__.py, check.py, doctor.py, init.py, migrate.py, outdated.py, publish.py, validate.py, plugin/. Your PR adds 112 lines to the now-deleted marketplace.py, so a rebase produces CONFLICT (modify/delete).
Resolution: re-port your command registration into the new package layout. Concretely, after picking a name (say audit), add src/apm_cli/commands/marketplace/audit.py and wire it from src/apm_cli/commands/marketplace/__init__.py next to the existing check, doctor, outdated, etc. registrations.
[!] 3. src/apm_cli/marketplace/client.py has evolved independently
Your PR refactors _try_proxy_fetch -> _try_proxy_fetch_raw and adds fetch_raw. Main has independently changed the same _try_proxy_fetch (modern dict | None return type, cfg.scheme / headers wiring). The conflict is resolvable but non-trivial -- please reconcile against current main rather than force-applying your version, so the proxy fetch path keeps the upstream improvements.
Naming proposals (you pick)
In rough order of our preference:
apm marketplace audit-- our recommendation. Cleanest semantic split from environmentdoctor, easy to document, no breaking change for existing users. Short, memorable, signals "supply-chain audit".- Fold into
apm marketplace check --strict-pinning-- attractive if you'd rather not introduce a new top-level subcommand. Trade-off:checkis currently lockfile drift / integrity oriented, so the flag would need a clear scope. - Rename existing env-diagnostic to
apm marketplace env-doctor, keepdoctorfor your auditor -- works semantically, but it's a breaking change to a shipped command and would need a deprecation shim plus a CHANGELOGBREAKINGentry.
Maintainers are happy with option 1. If you'd prefer 2 or 3, flag it in your next push and we'll align.
Doc requirements (repo doc-sync rule)
Any new CLI command must update the following in the same PR:
CHANGELOG.md-- new### Addedentry under[Unreleased], following the Keep a Changelog format already used in the file.docs/src/content/docs/reference/cli.md(or the closest existing page underdocs/src/content/docs/) -- one short section: synopsis, flags, exit codes, a one-line example.packages/apm-guide/.apm/skills/apm-usage/commands.md-- add the new command alongside the otherapm marketplace ...entries so the in-product guide stays in sync.
If option 3 above is chosen, also add a ### Changed (BREAKING) CHANGELOG entry for the doctor rename and update any docs that reference the existing marketplace doctor.
What's already great
This is a serious piece of work and we don't want any of it lost in the rebase:
classify_dependency-- clean separation between marketplace-pinned, bypassed, and ambiguous cases. Exactly the right primitive for this surface.- Dict-form bypass detection -- catching the
{name: ..., source: ...}long-form that side-steps marketplace pinning is the subtle case most reviewers would have missed. FetchStatusenum for fault isolation -- distinguishing "marketplace says no" from "network failed" from "auth missing" is the right call; it keeps the auditor honest under partial outages and makes exit-code semantics tractable.- Path-traversal guards -- aligned with the repo's
validate_path_segments/ensure_path_withinrule. Good instinct. - 592 lines of unit tests for
doctor.py-- coverage is genuinely thorough.
None of this needs to change. The blockers are all about where the code lives and what it's called, not what it does.
Next step
- Rebase onto current
main. - Pick a name (we recommend
audit). - Re-port the command into the new
src/apm_cli/commands/marketplace/package layout. - Reconcile
marketplace/client.pyagainst main's_try_proxy_fetchrather than overwriting it. - Add the three doc updates listed above.
- Re-request review.
If the rebase is painful (it will be), say the word in a comment and a maintainer will pair on it or push a rebase branch you can pull from. We'd rather help you across the line than see this stall.
Thanks again for the contribution.
8f1c9d4 to
ec91c42
Compare
|
@danielmeppiel thanks for the thorough review. I have rebased onto current How each blocker was resolved1. Naming collision ( 2. Target file deletion -- re-ported the command into the new 3. Doc sync (per repo rule)
TestsTotal 56 new tests, all passing alongside the existing suite (6982 passed):
Other small improvements made during the rebase
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new apm marketplace audit command to detect transitive dependencies.apm entries that bypass marketplace pinning by fetching each plugin’s apm.yml at its pinned ref and classifying dependency shapes.
Changes:
- Refactors marketplace client proxy fetching to expose a reusable
fetch_raw()primitive for arbitrary files at a ref. - Implements marketplace audit core logic + Click CLI command (
apm marketplace audit NAME, with--strict/-v). - Adds unit + CLI integration tests and updates user-facing docs + changelog.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/marketplace/test_marketplace_client.py | Adds focused tests for the new fetch_raw() error contract + proxy-only behavior. |
| tests/unit/marketplace/test_marketplace_audit.py | Adds extensive test coverage for dependency classification, dep normalization, plugin coord resolution, fetch error messaging, and CLI --strict behavior. |
| src/apm_cli/marketplace/client.py | Factors byte-level proxy fetch and introduces fetch_raw() for raw file retrieval with shared auth/proxy behavior. |
| src/apm_cli/marketplace/audit.py | Implements audit core: classification, dep collection/normalization, plugin GitHub coord resolution, per-plugin fetch/parse, and aggregation. |
| src/apm_cli/commands/marketplace/audit.py | Adds the apm marketplace audit command with summary output and CI-friendly --strict exit behavior. |
| src/apm_cli/commands/marketplace/init.py | Registers the new audit subcommand in the marketplace command group exports. |
| packages/apm-guide/.apm/skills/apm-usage/commands.md | Documents apm marketplace audit NAME in the commands table. |
| docs/src/content/docs/reference/cli-commands.md | Adds detailed CLI reference documentation for apm marketplace audit. |
| CHANGELOG.md | Announces the new command and its behavior/exit semantics. |
30efdd5 to
13f7e29
Compare
…lace pinning
A marketplace author pinning a package at a specific SHA still leaves
a supply-chain gap: if that package's own apm.yml declares
dependencies.apm using direct repo paths (e.g. owner/repo/path),
those transitive deps are resolved via git clone and track HEAD --
the marketplace's version pinning does not flow through them.
Add apm marketplace audit <name>:
- Fetches each plugin's own apm.yml at its pinned ref and warns when
a dependencies.apm entry would be resolved outside the marketplace
catalogue.
Classification:
- Bypasses (warn): bare owner/repo, owner/repo/subpath, https:// /
ssh:// git URLs, and { git: URL, ... } object-form entries.
- Clean: name@marketplace refs; local paths (./x, /abs, ../x).
- Skipped: plugin has no apm.yml at the pinned ref, or source type
is not an addressable github manifest.
- Unverifiable errors: fetch failure or malformed YAML (per-plugin
isolation -- one bad plugin does not abort the run).
Behavior:
- Default run is informational and exits 0. Suppresses the per-plugin
section header on an all-clean run so the summary is not preceded by
an empty section.
- --strict exits non-zero on any bypass warning or unverifiable
plugin, for use in CI.
- --verbose surfaces clean plugins and skipped reasons inline; on
errors, also prints the captured traceback for debugging.
Implementation notes:
- Lives at src/apm_cli/commands/marketplace/audit.py (new package
layout from microsoft#1024) and src/apm_cli/marketplace/audit.py (domain
logic). Wired into MarketplaceGroup as an Authoring command.
- Coexists with the existing apm marketplace doctor (environment
diagnostics) -- different mental model, different subcommand.
- Aligns with the existing apm audit (content integrity scan) under
the project's "audit = security/integrity" naming family.
- fetch_raw() is added as a new public primitive in marketplace/client.py
alongside the new _try_proxy_fetch_raw() helper. _try_proxy_fetch
and _fetch_file are reconciled with main: their public contracts
are preserved (returns dict | None, raises MarketplaceFetchError);
internally, _try_proxy_fetch now delegates to _try_proxy_fetch_raw
to keep proxy I/O DRY between marketplace.json and arbitrary-file
callers. No existing call site changes.
- fetch_raw() raises the neutral MarketplaceError base so audit can
surface its own per-plugin context instead of inheriting the
MarketplaceFetchError "run apm marketplace update" retry hint,
which is wrong at plugin granularity.
- Object-style dep entries ({ git: ..., path: ..., ref: ... } -- the
same shape DependencyReference.parse_from_dict accepts) are
normalized to strings before classification so they are not
silently dropped.
- Path traversal in the plugin source path field is rejected via the
existing validate_path_segments.
Tests (56 new):
- 53 in tests/unit/marketplace/test_marketplace_audit.py covering the
classifier, dep normalisation including dict-form, plugin coord
resolution, fetch error-message regression, and the CLI command
(default / --strict on bypass / --strict on unverifiable plugin /
unverifiable without strict / clean exits / --verbose output).
- 3 in tests/unit/marketplace/test_marketplace_client.py::TestFetchRaw
pinning the public contract of fetch_raw: raises neutral
MarketplaceError (not MarketplaceFetchError), short-circuits on
proxy hit, and respects PROXY_REGISTRY_ONLY=1 to keep direct GitHub
fetches off.
Docs:
- CHANGELOG.md: Added entry under [Unreleased].
- docs/src/content/docs/reference/cli-commands.md: full command
reference (synopsis, flags, classification, exit codes, examples,
note on bypassing the 1h marketplace.json cache).
- packages/apm-guide/.apm/skills/apm-usage/commands.md: in-product
guide entry alongside other marketplace authoring commands.
Fixes microsoft#847
13f7e29 to
e7b603d
Compare
|
Pushed Addressed in code(items 2-4) (item 7) Also caught during the rebase
Not changed(items 5-6) (item 1) The "doctor"/"audit" + Status
|
Description
A marketplace author pinning a package at a specific SHA still leaves a supply-chain gap: if that package's own
apm.ymldeclaresdependencies.apmusing direct repo paths (e.g.owner/repo/path), those transitive deps are resolved via git clone and track HEAD -- the marketplace's version pinning does not flow through them.This PR adds
apm marketplace audit <name>that fetches each plugin's ownapm.ymlat its pinned ref and warns when adependencies.apmentry would be resolved outside the marketplace catalogue.Classification
owner/repo,owner/repo/subpath,https:///ssh://git URLs, and{ git: URL, ... }object-form entries.name@marketplacerefs; local paths (./x,/abs,../x).apm.ymlat the pinned ref, or source type is not an addressable github manifest.Behavior
--strictexits non-zero on any bypass warning or unverifiable plugin, for use in CI.--verbosesurfaces clean plugins and skipped reasons inline; on errors it also prints the captured traceback for debugging.Type of change
Implementation notes
src/apm_cli/commands/marketplace/audit.py(new package layout from refactor: split marketplace commands into package modules #1024) for the CLI surface andsrc/apm_cli/marketplace/audit.pyfor domain logic. Wired intoMarketplaceGroupas an Authoring command betweenoutdatedanddoctor.apm marketplace doctor(environment diagnostics) -- different mental model, different subcommand. Aligns with the existing top-levelapm audit(content integrity scan) under the project's "audit = security/integrity" naming family.fetch_raw()is added as a new public primitive inmarketplace/client.pyalongside the new_try_proxy_fetch_raw()helper._try_proxy_fetchand_fetch_fileare reconciled withmain: their public contracts are preserved (returnsdict | None, raisesMarketplaceFetchError); internally,_try_proxy_fetchnow delegates to_try_proxy_fetch_rawto keep proxy I/O DRY between marketplace.json and arbitrary-file callers. No existing call site changes.fetch_raw()raises the neutralMarketplaceErrorbase so audit can surface its own per-plugin context instead of inheriting theMarketplaceFetchError"run apm marketplace update" retry hint, which is wrong at plugin granularity.{ git: ..., path: ..., ref: ... }-- the same shapeDependencyReference.parse_from_dictaccepts) are normalized to strings before classification so they are not silently dropped.pathfield is rejected via the existingvalidate_path_segments.Review feedback addressed
Per @danielmeppiel's review:
doctor->audit(option 1). The existingapm marketplace doctor(environment diagnostics) is unchanged.src/apm_cli/commands/marketplace/package asaudit.py, registered in__init__.pyalongsidecheck,outdated, etc., and added toMarketplaceGroup._authoring_commands.marketplace/client.pyreconciled againstmain: kept_try_proxy_fetchsemantically identical (signature, return type, JSON-decode-on-bytes behavior), added_try_proxy_fetch_rawas a sibling helper that_try_proxy_fetchnow delegates to._fetch_fileis unchanged frommain. The existingtest_marketplace_client.pypatches at_try_proxy_fetchcontinue to work without modification.CHANGELOG.md-- new### Addedentry under[Unreleased].docs/src/content/docs/reference/cli-commands.md-- full command reference (synopsis, flags, classification, exit codes, examples).packages/apm-guide/.apm/skills/apm-usage/commands.md-- in-product guide entry alongside other marketplace authoring commands.Testing
uv run pytest tests/unit tests/test_console.py-- 6982 passed; the preexisting environment-specifictest_is_tool_availableflake is unrelated).tests/unit/marketplace/test_marketplace_audit.pycovering the classifier, dep normalisation including dict-form, plugin coord resolution, fetch error-message regression, and the CLI command (default /--stricton bypass /--stricton unverifiable plugin / unverifiable without strict / clean exits /--verboseoutput).tests/unit/marketplace/test_marketplace_client.py::TestFetchRawpinning the public contract offetch_raw: raises neutralMarketplaceError(notMarketplaceFetchError), short-circuits on proxy hit, and respectsPROXY_REGISTRY_ONLY=1to keep direct GitHub fetches off.Manual verification
End-to-end run of
apm marketplace auditexercising the full CLI through real Click + realrun_audit/classify_dependency/fetch_plugin_apm_ymlorchestration with only network boundaries stubbed, across:{ git: https:// ... }, dict-form{ git: git@... }SSH -- all flaggedapm.yml(skipped), malformed YAML (parse error), HTTP 5xx (network error)--strictexit code,--verboseclean+skipped detail, summary counts, conditional section header on all-cleanAdditionally,
apm marketplace add(which goes throughfetch_marketplace->_fetch_file->_try_proxy_fetch->_try_proxy_fetch_raw) was confirmed against the real GitHub API to ensure the client.py reconciliation did not break the live network path used by all existing marketplace consumer commands.Fixes #847