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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`apm compile -t copilot` now emits `.github/copilot-instructions.md` with zero user configuration** -- APM's first Copilot-native compile target. Global instructions in `.apm/instructions/` are assembled into the file VS Code and GitHub Copilot read automatically; switching targets cleans it up. APM dogfoods this target. (#1048)
- **`apm marketplace add` accepts full HTTPS URLs and nested HOST/group/sub/.../REPO shorthands.** You can now paste a repository URL straight from the browser (e.g., `apm marketplace add https://github.com/acme/plugin-marketplace`) and register marketplaces hosted under nested sub-paths on GitHub Enterprise (`ghes.corp.example.com/org/team/repo`). Path-traversal sequences in the parsed segments are rejected via `validate_path_segments`. Non-GitHub hosts (GitLab, Bitbucket, etc.) are explicitly rejected at registration time with an actionable error -- this avoids forwarding GitHub credentials to unintended hosts and the silent fetch-time 404 that previously resulted; native non-GitHub support is tracked separately. (#1034, closes #1027)
- Regression tests for `apm compile` placement of narrow `applyTo` patterns: instructions whose matches all live deep inside one subtree are now pinned to the deepest covering directory instead of being hoisted to the project root, across both selective and single-point placement strategies. Also covers the file-walk cache that skips repeated filesystem scans for the same glob. (#871)
- **`apm marketplace audit <name>`** -- supply-chain audit that 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 (direct repo paths, git URLs, or `{git: ...}` object-form entries). Default run is informational and exits 0; `--strict` exits non-zero on bypass warnings or unverifiable plugins, for use in CI. Complements the existing `apm marketplace doctor` (environment diagnostics). (#881)
Comment thread
edenfunf marked this conversation as resolved.
- **`apm pack` marketplace builder hardening.** Local source paths are now emitted relative to `metadata.pluginRoot` (fixes double-prefix bug). New pass-through fields: `author`, `license`, `repository`, `keywords` (alias for `tags`). Curator-wins override semantics for `description`/`version` on remote entries. Security guards reject path traversal and absolute paths post-subtraction. (#1061)
- **Plugin manifest schema-conformance tests.** `tests/unit/test_plugin_exporter_schema.py` validates every shape of `plugin.json` produced by `apm pack` (synthesized, authored, and authored-with-stale-keys) against the vendored official schema. Companion marketplace conformance lives in `tests/unit/marketplace/test_schema_conformance.py`. (#1061)
- **APM now compiles and integrates to Windsurf/Cascade.** New first-class `--target windsurf` support: instructions deploy as `.windsurf/rules/` with trigger frontmatter, agents deploy as `.windsurf/skills/<name>/SKILL.md`, commands as `.windsurf/workflows/`, hooks merge into `.windsurf/hooks.json`, and MCP servers configure via `~/.codeium/windsurf/mcp_config.json`. Auto-detection, user-scope deployment, and `apm pack` all support the new target. (#1066)
Expand Down
34 changes: 34 additions & 0 deletions docs/src/content/docs/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -1421,6 +1421,40 @@ apm marketplace check
apm marketplace check --offline
```

#### `apm marketplace audit` - Audit transitive dependencies for marketplace bypass

Fetch each plugin's own `apm.yml` at its pinned ref and warn when a `dependencies.apm` entry would be resolved outside the marketplace catalogue. Such transitive deps would resolve via direct git clone and track HEAD, defeating the supply-chain pinning the marketplace provides.

The marketplace manifest is always re-fetched fresh (the 1-hour `marketplace.json` cache is bypassed) so the audit is run against the current published catalogue rather than stale local data.

```bash
apm marketplace audit NAME [OPTIONS]
```

**Arguments:**
- `NAME` - Registered marketplace name (required)

**Options:**
- `--strict` - Exit non-zero on any bypass warning or unverifiable plugin (CI-friendly)
- `-v, --verbose` - Show clean plugins and skipped reasons inline

**Classification:**
- *Bypasses (warn)* - bare `owner/repo`, `owner/repo/subpath`, `https://`/`ssh://` git URLs, and `{git: URL, ...}` object-form entries
- *Clean* - `name@marketplace` refs and local paths (`./x`, `/abs`, `../x`)
- *Skipped* - plugin has no `apm.yml` at the pinned ref, or the source type is not an addressable github manifest
- *Unverifiable* - fetch failure or malformed YAML (per-plugin isolation: one bad plugin does not abort the run)

**Exit codes:**
- `0` - Default mode always exits 0; `--strict` exits 0 only when nothing bypasses and no plugin is unverifiable
- `1` - `--strict` mode with bypass warnings or unverifiable plugins

**Examples:**
```bash
apm marketplace audit my-marketplace
apm marketplace audit my-marketplace --strict # CI gate
apm marketplace audit my-marketplace -v # show clean & skipped detail
```

#### `apm marketplace doctor` - Environment diagnostics

Check git, network reachability, authentication, `gh` CLI availability, and the presence of a marketplace config (in `apm.yml` or legacy `marketplace.yml`). Run this first when `apm pack` or `publish` fails in an unfamiliar environment.
Expand Down
1 change: 1 addition & 0 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
| `apm marketplace migrate` | Fold a legacy `marketplace.yml` into `apm.yml`'s `marketplace:` block; deletes `marketplace.yml` on success | `--force`/`--yes`/`-y`, `--dry-run`, `-v` |
| `apm marketplace outdated` | Report upgradable plugins, range-aware | `--offline`, `--include-prerelease`, `-v` |
| `apm marketplace check` | Validate the `marketplace:` block and verify refs resolve | `--offline`, `-v` |
| `apm marketplace audit NAME` | Supply-chain audit: warn when plugin transitive deps bypass marketplace pinning | `--strict` (CI exit-1 on bypass), `-v` |
| `apm marketplace doctor` | Diagnose git, network, auth, and marketplace config readiness | `-v` |
| `apm marketplace publish` | Open PRs on consumer repos from `consumer-targets.yml` | `--targets PATH`, `--dry-run`, `--no-pr`, `--draft`, `--allow-downgrade`, `--allow-ref-change`, `--parallel N`, `-y` |
| `apm marketplace package add <source>` | Add a plugin entry to `marketplace.plugins` (source accepts `owner/repo` or `./path`) | `--name`, `--version`, `--ref` (mutable refs auto-resolved to SHA), `-d`/`--description`, `-s`/`--subdir`, `--tag-pattern`, `--tags`, `--include-prerelease`, `--no-verify` |
Expand Down
3 changes: 3 additions & 0 deletions src/apm_cli/commands/marketplace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class MarketplaceGroup(click.Group):
"init",
"check",
"outdated",
"audit",
"doctor",
"publish",
"package",
Expand Down Expand Up @@ -1342,6 +1343,7 @@ def search(expression, limit, verbose):
sys.exit(1)


from .audit import audit # noqa: E402
from .check import check # noqa: E402
from .doctor import doctor # noqa: E402
from .init import init # noqa: E402
Expand Down Expand Up @@ -1382,6 +1384,7 @@ def search(expression, limit, verbose):
"SemVer",
"TargetResult",
"add",
"audit",
"browse",
"check",
"detect_config_source",
Expand Down
127 changes: 127 additions & 0 deletions src/apm_cli/commands/marketplace/audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""``apm marketplace audit`` command."""

from __future__ import annotations

import sys
import traceback

import click

from ...core.command_logger import CommandLogger
from ...marketplace.audit import FetchStatus, run_audit
from ...marketplace.client import fetch_marketplace
from ...marketplace.registry import get_marketplace_by_name
from . import marketplace


@marketplace.command(
help="Audit plugin transitive deps for marketplace-bypass risk"
)
@click.argument("name", required=True)
@click.option(
"--strict",
is_flag=True,
help="Exit non-zero when any plugin has bypass dependencies or fetch errors",
)
@click.option("--verbose", "-v", is_flag=True, help="Show detailed output")
def audit(name, strict, verbose):
"""Audit a registered marketplace's supply-chain pinning.

For each plugin in ``NAME``'s manifest, fetch the plugin's own
``apm.yml`` (at its pinned ref) and warn when ``dependencies.apm``
entries use direct repo paths, which bypass the marketplace's
version pinning and make transitive deps track HEAD.
"""
logger = CommandLogger("marketplace-audit", verbose=verbose)
try:
source = get_marketplace_by_name(name)
logger.start(f"Auditing marketplace '{name}'...", symbol="gear")

manifest = fetch_marketplace(source, force_refresh=True)
n = len(manifest.plugins)
logger.progress(
f"Checking {n} plugin{'' if n == 1 else 's'}...", symbol="info"
)

reports = run_audit(manifest, source)

ok_count = 0
bypass_total = 0
fetch_error_count = 0
skipped_count = 0

# Suppress the per-plugin section header when there is nothing to
# report and the user did not opt into verbose: in the all-clean
# default run the header would otherwise hang above an empty body.
has_findings = any(
rep.fetch_status != FetchStatus.OK or rep.issues
for rep in reports
)

click.echo()
if has_findings or verbose:
click.echo("Audit Results:")
for rep in reports:
if rep.fetch_status == FetchStatus.OK:
if not rep.issues:
ok_count += 1
if verbose:
logger.success(
f" {rep.plugin_name}: deps are marketplace-resolved",
symbol="check",
)
continue
bypass_total += len(rep.issues)
if len(rep.issues) == 1:
verb_phrase = "1 dependency bypasses"
else:
verb_phrase = f"{len(rep.issues)} dependencies bypass"
logger.warning(
f" {rep.plugin_name}: {verb_phrase} the marketplace",
symbol="warning",
)
for issue in rep.issues:
click.echo(f" - '{issue.dep}'")
click.echo(f" hint: {issue.suggestion}")
elif rep.fetch_status in (
FetchStatus.NO_MANIFEST,
FetchStatus.UNSUPPORTED_SOURCE,
):
skipped_count += 1
if verbose:
logger.verbose_detail(
f" {rep.plugin_name}: skipped ({rep.fetch_status.value}"
f"{' - ' + rep.detail if rep.detail else ''})"
)
else:
fetch_error_count += 1
logger.warning(
f" {rep.plugin_name}: could not verify "
f"({rep.fetch_status.value}: {rep.detail})",
symbol="warning",
)

click.echo()
warn_noun = "warning" if bypass_total == 1 else "warnings"
err_noun = "error" if fetch_error_count == 1 else "errors"
click.echo(
f"Summary: {ok_count} clean, {bypass_total} bypass {warn_noun}, "
f"{skipped_count} skipped, "
f"{fetch_error_count} unverifiable {err_noun}"
)
if bypass_total:
click.echo()
click.echo(
"Marketplace refs (name@marketplace) pin transitive deps "
"through the catalogue so consumers get the same versions "
"you tested. See: "
"https://microsoft.github.io/apm/guides/marketplaces/"
)

if strict and (bypass_total or fetch_error_count):
sys.exit(1)

except Exception as e:
logger.error(f"Failed to audit marketplace: {e}")
logger.verbose_detail(traceback.format_exc())
sys.exit(1)
Loading