feat: support allow-insecure HTTP dependencies - #700
feat: support allow-insecure HTTP dependencies#700Daniel Meppiel (danielmeppiel) merged 25 commits into
Conversation
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR introduces an explicit opt-in flow for allowing HTTP (insecure) APM dependencies, including persisting that decision in apm.yml and preserving HTTP metadata during lockfile replays.
Changes:
- Add
--allow-insecureflag + globalallow-insecureconfig to gate installation ofhttp://dependencies. - Persist and replay HTTP dependency metadata (
is_insecure,allow_insecure) via manifest and lockfile. - Update docs and add unit tests covering HTTP parsing, lockfile round-trip, CLI behaviors, and cloning behavior.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_install_update.py | Adds lockfile replay/round-trip tests for insecure HTTP metadata. |
| tests/unit/test_install_command.py | Adds CLI tests for --allow-insecure and install-time security checks. |
| tests/unit/test_config_command.py | Adds config command + config function tests for allow-insecure. |
| tests/unit/test_canonicalization.py | Adds parsing/serialization tests for HTTP dependency handling. |
| tests/unit/test_auth_scoping.py | Ensures HTTP deps don’t fall back to SSH during clone attempts. |
| src/apm_cli/models/dependency/reference.py | Introduces is_insecure/allow_insecure, HTTP canonicalization, manifest serialization, and scheme-aware URLs. |
| src/apm_cli/drift.py | Preserves insecure scheme info during lockfile replay. |
| src/apm_cli/deps/lockfile.py | Persists is_insecure/allow_insecure in lockfile read/write and dependency refs. |
| src/apm_cli/deps/github_downloader.py | Adds HTTP-only clone path and URL building for insecure deps. |
| src/apm_cli/config.py | Adds get_allow_insecure / set_allow_insecure. |
| src/apm_cli/commands/install.py | Adds --allow-insecure, manifest writing behavior, and install-time HTTP enforcement. |
| src/apm_cli/commands/config.py | Generalizes config get/set to support allow-insecure. |
| src/apm_cli/commands/_helpers.py | Propagates HTTP/artifactory/local fields when computing install paths. |
| src/apm_cli/bundle/plugin_exporter.py | Propagates HTTP/artifactory/local fields for exported install paths. |
| packages/apm-guide/.apm/skills/apm-usage/commands.md | Documents --allow-insecure in the command reference table. |
| docs/src/content/docs/reference/cli-commands.md | Documents --allow-insecure and new config key. |
| docs/src/content/docs/guides/dependencies.md | Documents HTTP dependency manifest format + caution guidance. |
101a07a to
3a28c8c
Compare
Sergio Sisternes (sergio-sisternes-epam)
left a comment
There was a problem hiding this comment.
Solid feature -- good security model with dual opt-in (dep-level + CLI/config), thorough test coverage (30+ tests), clean lockfile round-trip, and well-written docs. Nice work!
One blocking issue: to_apm_yml_entry() hardcodes allow_insecure = True, which could silently escalate security posture. See inline comment.
Also: this PR silently fixes a bug where artifactory_prefix was missing from DependencyReference construction in _helpers.py, plugin_exporter.py, and lockfile.py -- directly related to #614. Consider referencing that issue in the PR description.
| entry["ref"] = self.reference | ||
| if self.alias: | ||
| entry["alias"] = self.alias | ||
| entry["allow_insecure"] = True |
There was a problem hiding this comment.
Blocking: This hardcodes allow_insecure = True for all HTTP deps, ignoring self.allow_insecure. If this method is reused in a code path that hasn't applied the opt-in check, it silently grants insecure access.
| entry["allow_insecure"] = True | |
| entry["allow_insecure"] = self.allow_insecure |
| ado_project=ado_project, | ||
| ado_repo=ado_repo, | ||
| artifactory_prefix=artifactory_prefix, | ||
| is_insecure=dependency_str.startswith("http://"), |
There was a problem hiding this comment.
Suggestion: startswith("http://") is case-sensitive, but URL schemes are case-insensitive per RFC 3986. HTTP://host/repo would bypass insecure detection.
| is_insecure=dependency_str.startswith("http://"), | |
| is_insecure=urllib.parse.urlparse(dependency_str).scheme.lower() == "http", |
| return build_ssh_url(host, repo_ref) | ||
| elif is_github and github_token: | ||
| # Only send GitHub tokens to GitHub hosts | ||
| # # Only send GitHub tokens to GitHub hosts |
There was a problem hiding this comment.
Nit: Double # — typo from the refactor commit.
| # # Only send GitHub tokens to GitHub hosts | |
| # Only send GitHub tokens to GitHub hosts |
| sub_path = entry.get("path") | ||
| ref_override = entry.get("ref") | ||
| alias_override = entry.get("alias") | ||
| allow_insecure = bool(entry.get("allow_insecure", False)) |
There was a problem hiding this comment.
Suggestion: bool(entry.get("allow_insecure", False)) treats any non-empty string (e.g., "false") as True. Since this is a security-relevant field, consider validating the type explicitly:
| allow_insecure = bool(entry.get("allow_insecure", False)) | |
| allow_insecure = entry.get("allow_insecure", False) | |
| if not isinstance(allow_insecure, bool): | |
| raise ValueError("'allow_insecure' field must be a boolean") |
|
Sergio Sisternes (@sergio-sisternes-epam) On #614: this PR now explicitly passes |
Sergio Sisternes (sergio-sisternes-epam)
left a comment
There was a problem hiding this comment.
All findings addressed — nice work, arika (@arika0093)! Each fix is clean and well-tested.
Thanks for the clarification on #614 — you're right that the artifactory_prefix propagation fix here is a partial improvement but the core _parse_artifactory_base_url() issue remains separate.
LGTM!
Daniel Meppiel (danielmeppiel)
left a comment
There was a problem hiding this comment.
First — thanks for tackling this, arika (@arika0093). The use case is real (internal mirrors without HTTPS exist in the wild), the dual opt-in design (per-dep allow_insecure: true + CLI flag) is the right shape, and the test coverage is thorough. Sergio's APPROVE isn't wrong on the implementation quality.
I'm requesting changes from a security/UX lens because APM is a supply-chain tool, and a few aspects of the current design create footguns that I don't think we can ship to the broader community. None of these are deal-breakers — they're scope tightening. If we land them, this becomes a feature I'd be proud to ship.
Blockers
1. Drop apm config set allow-insecure true (the persistent global config)
This is the biggest single risk in the PR and, IMO, the only true blocker.
Per-invocation --allow-insecure is the right friction ceiling. A persistent global setting:
- Means a developer enables it once for one internal project and then forever silently allows HTTP for every subsequent
apm installon that machine, including in unrelated repos. - Turns shared CI runners into a network-MITM surface for any repo that lands
allow_insecure: trueinapm.yml— the runner doesn't need to opt in per-job, the maintainer who set the config months ago already did. - Is invisible at install time. There's no per-install reminder. Users forget they set it.
- Converts a per-action security decision into ambient permission, which is the textbook supply-chain footgun.
The per-invocation flag is the correct model. It's friction, but the friction is the security feature: every HTTP install requires an active, conscious choice. Please drop the apm config set allow-insecure path entirely (and the corresponding get_allow_insecure / set_allow_insecure config functions).
2. Be loud at install time about which URLs are insecure
Today, the user only sees output about HTTP when --allow-insecure is missing (the error path). On the success path there's no equivalent loud diagnostic listing exactly which URLs are about to be fetched over an unauthenticated channel.
Compare to pip's behavior on insecure indexes: a clear warning per fetch. APM should do the same.
Concretely:
- Print one
[!]line per HTTP dependency at install-prepare time, with the full URL:[!] Fetching insecurely (no transport auth): http://internal.example.com/team/foo - Make this print whether or not
--allow-insecurewas passed (i.e. the warning is informational; the flag is the gate). - This is what makes Alice notice when Bob's
apm.ymlgrew an unexpected HTTP entry betweengit pulls.
3. Transitive HTTP dependencies need a separate gate
This is the most subtle one and the one I'd most appreciate your thinking on. Scenario:
- Bob adds a top-level dep
http://internal.bob.example/foo/barwithallow_insecure: trueto the project'sapm.yml. Alice can see this in the diff and consents by passing--allow-insecure. - But Bob's package itself has an
apm.ymlthat pulls a transitive dephttp://attacker-controlled.example/lib/xover HTTP, also withallow_insecure: true. - When Alice runs
apm install --allow-insecure, that transitive dep gets fetched too. Alice never saw it in the project's ownapm.yml. Her consent at the root extends silently to a graph she didn't review.
The lockfile makes this auditable after it's pinned, but the first install (and any update) doesn't surface this distinction.
Suggested approach (open to alternatives):
- Track per-dep whether the HTTP+
allow_insecurechoice originated in the rootapm.ymlor in a transitive dep. - For transitive HTTP, require an additional explicit acknowledgment: either a separate
--allow-insecure-transitiveflag, or an interactive prompt listing the transitive HTTP URLs and their introducing parent, with--yesto skip in CI. - At minimum, the per-URL warning from #2 should annotate
(transitive, introduced by <parent-dep>)so users can see it.
Without this, the dual-opt-in only protects the immediate dep — the transitive surface is a single root flag away from being silently expanded.
Non-blocking but please consider
- Threat-model docs: the new HTTP section in
dependencies.mddocuments the mechanism but not the why. A short paragraph explaining that HTTP has no transport auth (so an MITM can substitute the package contents — the SHA in the lockfile is itself MITM-able when fetched over HTTP) would help users make informed choices instead of copy-pasting the example. - Audit affordance: there's no way to ask "list every insecure dep in my dependency graph" before running install. With the lockfile already preserving
is_insecure, anapm deps list --insecure(orapm install --dry-runlisting them) would be cheap and high-value. Could be a follow-up. - CHANGELOG entry: this is a security-sensitive feature. The CHANGELOG entry should be explicit that this introduces an opt-in HTTP path with the threat-model implications, not just "feat: support http deps."
- Hostname allowlist (future, not now): a future iteration could let
apm.ymldeclare aninsecure_hosts: [...]allowlist so that HTTP is only permitted from explicitly-named hosts. Not for this PR.
What I'd merge
If you land #1 (drop global config) and #2 (loud per-URL output), I'd merge. #3 (transitive gating) is the one I feel strongest about from a supply-chain perspective; if you'd prefer to defer it, I'd accept that as a follow-up issue only if the per-URL warning from #2 explicitly annotates transitive HTTP deps so the information is at least visible.
If you'd rather not drop the global config, I'd respectfully decline this PR and recommend users set up HTTPS on their internal mirrors. APM is a supply-chain tool and persistent ambient "allow insecure" is incompatible with that role.
Thanks again for the thoughtful design and the thorough tests — this is salvageable and worth landing well.
|
Thanks for the thoughtful review. I think your concerns are reasonable, especially given that APM is a supply-chain tool. I agree on the first two blockers:
On transitive HTTP dependencies: I agree this is the trickiest part. A root-level opt-in should not silently bless an unreviewed transitive graph. After thinking more about it, I do not think a simple For this PR, my plan is:
I think this is a better narrow fix for the security concern raised here, while also fitting the common internal-repository use case more naturally than a boolean transitive flag. I will also update the docs and changelog accordingly. The audit affordance ( Thanks again for the careful review. |
dbb6977 to
1796653
Compare
Daniel Meppiel (danielmeppiel)
left a comment
There was a problem hiding this comment.
APM Review Panel -- PR #700
[x] Verdict: REQUEST CHANGES -- one CRITICAL credential-leak finding plus a converging architectural/security issue around HTTP identity. The dual-opt-in design is sound and Sergio's earlier blocker is correctly fixed; the items below are what stand between this and merge.
arika (@arika0093) -- this is a substantial, security-sensitive contribution and the design instincts are right. Five reviewers (python-architect, cli-logging-expert, devx-ux-expert, supply-chain-security-expert; growth-hacker side-channel) converged on a tight set of must-fixes. Thank you for the depth here -- 1338 lines + 30+ tests on a class of feature most projects ship sloppily.
What's excellent
- Dual opt-in (manifest
allow_insecure: true+ CLI--allow-insecure) is the right mental model. Matches npmoverrides+--legacy-peer-deps. Neither alone is sufficient -- correct. - Persistent global config dropped per earlier review feedback. Right call; every install run should be a conscious act.
--allow-insecure-hostfor transitive deps with same-host auto-propagation for direct deps is well-designed. Good FQDN validation prevents flag injection.parse_from_dict()validatesallow_insecureis boolean -- prevents YAML type confusion.- Doc surface coverage (cli-commands.md, dependencies.md, apm-guide skill resources, CHANGELOG) is exactly the discipline this codebase demands.
Required changes (must land in this PR)
-
[x] CRITICAL -- HTTP clone leaks credentials in plaintext.
github_downloader.py_env_for(use_token=False)stripsGIT_ASKPASSandGIT_CONFIG_NOSYSTEMfor HTTP attempts, re-enabling system git credential helpers (macOS Keychain, Windows Credential Manager,gh auth). When git cloneshttp://internal.example.com/owner/repo, credential helpers may resolve stored tokens for that host and embed them as plaintextAuthorizationheaders. The existing testtest_insecure_http_dep_is_strict_by_defaultasserts"GIT_ASKPASS" not in env_used-- that test is verifying the vulnerability.Fix: HTTP attempts must actively block credential resolution:
if attempt.scheme == "http": env = dict(self.git_env) env['GIT_ASKPASS'] = 'echo' env['GIT_TERMINAL_PROMPT'] = '0' env['GIT_CONFIG_NOSYSTEM'] = '1' # Suppress credential helpers explicitly env['GIT_CONFIG_COUNT'] = '1' env['GIT_CONFIG_KEY_0'] = 'credential.helper' env['GIT_CONFIG_VALUE_0'] = '' return env
Update the test to assert credential-helper suppression for HTTP rather than its absence.
-
[x] BLOCKER -- HTTP/HTTPS identity collision enables transport downgrade.
get_unique_key()andget_identity()are scheme-blind, anddrift.pydocuments "Source/host/scheme changes -- not detected." That meanshttps://gitlab.com/team/rulesandhttp://gitlab.com/team/rulescollide as the same dep. An attacker submitting a PR that flips the scheme + addsallow_insecure: trueproduces zero lockfile drift signal. Combined with finding #1, this is a clean transport-downgrade exploit chain.Fix: Either (a)
get_identity()includes scheme whenis_insecure=True, or (b)drift.pytreats ais_insecureflip as drift requiring--update. Option (b) is the smaller change and preserves identity stability for non-HTTP deps. -
[x] BLOCKER -- Inverted import:
install/phases/resolve.pyimports fromcommands/install.py.resolve.py:297-300:from apm_cli.commands.install import ( _check_insecure_dependencies, _collect_insecure_dependency_infos, _guard_transitive_insecure_dependencies, _warn_insecure_dependencies, )
Domain code (
install/phases/) must never import from CLI command code. Extract the ~200 lines of insecure-policy logic +_InsecureDependencyInfodataclass into a newsrc/apm_cli/install/insecure_policy.py. Bothcommands/install.pyandinstall/phases/resolve.pythen import from the new module. Pure move, zero logic change. -
[x] BLOCKER --
drift.pypropagatesis_insecurebut notallow_insecureduring lockfile replay. Lines +250-252:build_download_ref()copiesis_insecure=Truebut dropsallow_insecure. The replayedDependencyReferencehasis_insecure=True, allow_insecure=False, which_check_insecure_dependencies()rejects. Lockfile replays of HTTP deps are broken (or worse, bypass the check via a different code path).if getattr(locked_dep, "is_insecure", False) is True: overrides["is_insecure"] = True overrides["allow_insecure"] = getattr(locked_dep, "allow_insecure", False)
-
[x] BLOCKER --
to_canonical()no longer stripshttp://, breaking the canonical identity contract. Architect + DevX both flagged this. The docstring update silently removes "no https://" and HTTP deps now producehttp://host/owner/repowhile HTTPS still strips. Every consumer that doesdep.to_canonical().split("/")or assumes no://will silently diverge for HTTP deps. Fix: keepto_canonical()scheme-free. The transport-aware string already exists asto_apm_yml_entry()-- use that for serialization paths and any surface that needs the scheme. -
[!] HIGH -- Add
LockedDependency.to_dependency_ref()factory. Three sites reconstructDependencyReferencefromLockedDependency:_helpers.py:138,plugin_exporter.py:399,lockfile.py:365. This PR already had to fix the missingartifactory_prefix(#614 root cause) AND addis_insecure/allow_insecureat all three. Per the "abstract when 3+ call sites" rule, add a factory method that maps every field once. This is the structural fix Sergio asked about -- the artifactory_prefix patch is a band-aid without it. -
[!] HIGH -- Unify the three competing error messages into one canonical recipe. Today a user sees three different errors depending on which gate failed:
- validation-time ("Pass
--allow-insecure"), - install-time/manifest-missing ("set allow_insecure: true"),
- install-time/flag-missing ("Pass
--allow-insecure").
None give the complete recipe. Collapse to one message that always names both requirements:
[x] http://my-server.example.com/owner/repo -- HTTP dependency (no transport encryption) To install: 1. Set allow_insecure: true on the dep in apm.yml 2. Pass --allow-insecure to apm installAnd use the full URL (with scheme) in every error -- the user needs to see why it's insecure.
- validation-time ("Pass
-
[!] HIGH -- Update
docs/src/content/docs/enterprise/security.md. This PR introduces an entirely new transport surface and threat model; per the repo rule ("If a code change weakens or contradicts any guarantee in security.md, the doc must be updated in the same PR"), security.md needs an "HTTP (insecure) dependencies" section covering: whatallow_insecuredoes/doesn't protect against, MITM impact on content-hash provenance, credential-helper isolation requirement, the transitive-host boundary model. -
[!] HIGH -- Logging routing: dead
else: _rich_*()fallback branches._warn_insecure_dependencies,_guard_transitive_insecure_dependencies,_check_insecure_dependenciesrepeatif logger: logger.X() else: _rich_X(...)7 times. Callers always have a logger; the else branches are dead code and bypassDiagnosticCollectorif ever triggered. Makeloggera required positional arg; drop every fallback branch.
Polish (same PR if cheap; follow-up otherwise)
-
[i] Library code calls
sys.exit(1)._check_insecure_dependenciesand_guard_transitive_insecure_dependenciesshould raise an exception; CLI layer catches and exits. Improves testability and reuse. -
[i]
_format_insecure_dependency_warninguses jargon ("no transport auth"). Reword:[!] Insecure HTTP fetch (unencrypted): http://... -
[i] Transitive-block error is a policy lecture. Drop the middle sentence; lead with the command. Move the "why" to docs.
-
[i]
apm deps list --insecurecolumn label "HTTP" misleads (values aredirect/via <parent>). Rename toOrigin. -
[i] Unrelated cleanups (config.py refactor,
_setup_git_environmentSSH cleanup) belong in separate commits for bisectability.
Worth a follow-up issue (don't block this PR)
APM_ALLOW_INSECURE=1env var counterpart for CI ergonomics (mirrorAPM_ALLOW_PROTOCOL_FALLBACK=1pattern). Not adding this is fine for v1; tracking issue acceptable.- Drift-detection enhancement: warn on first-time HTTP install without lockfile SHA pin (defense-in-depth against the hash-circular-trust limitation).
Merge call
This is solid first-PR work on a security-sensitive feature -- the dual-opt-in design and host scoping are correct. The blockers are concentrated in three areas: credential leakage (#1), identity model (#2, #5), and architectural cleanup (#3, #4, #6). Items #1-#5 are the merge-blockers; #6-#9 should land in the same PR; the rest is polish or follow-up. Please address #1-#9 in this PR and re-request review.
Growth angle
- Real adoption blocker unlocked: corporate Gitea/Gogs/Artifactory-on-HTTP and air-gapped enterprise users -- the segment that quietly worked around APM's "install from anywhere" claim with manual
git clone. Worth a release-notes call-out. - First-time NONE-association contributor shipping 1338 lines + 30+ tests on a security feature is the contributor signal worth amplifying once this lands. Suggested release-notes line: "APM now supports private HTTP git hosts with explicit dual-gate opt-in (manifest + CLI). Insecure by default? Never. Insecure when you need it, with a clear audit trail." Repostable hook: "APM doesn't block your infra -- it makes insecure choices visible."
- Once shipped, security.md becomes the authoritative reference for the threat model -- making the doc update in #8 above doubly load-bearing.
Reviewed via the apm-review-panel skill: python-architect, cli-logging-expert, devx-ux-expert, supply-chain-security-expert; arbitrated by apm-ceo with oss-growth-hacker side-channel.
1796653 to
803a126
Compare
…split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… 14 stack split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ck split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tack split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…m 10) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
b2a0a78 to
b8a5c5f
Compare
|
Thanks for the detailed review. I addressed all requested changes from this review, rewrote the stack so the commits are easier to map back to the review items, and kept the split granularity intact. Review item mapping:
For review item 14:
At this point, all review items from this review have been addressed in the branch. |
The combined surface of PR microsoft#700 (allow-insecure CLI flags + policy) and main's MCP install block (microsoft#810, microsoft#814) pushed commands/install.py over the 1525 LOC invariant enforced by tests/unit/install/ test_architecture_invariants. Per the test's own guidance, resolved by extracting -- not trimming. Conflicts: - CHANGELOG.md: unioned the PR microsoft#700 Added entry with main's MCP entries. - packages/apm-guide/.apm/skills/apm-usage/commands.md: merged both flag lists into a single --allow-insecure + --mcp row. - src/apm_cli/commands/install.py: unioned the install() signature and docstring examples; kept both the InsecureDependencyPolicyError and click.UsageError except branches. - tests/unit/test_install_command.py: kept TestAllowInsecureFlag and TestInstallMcpFlag as sibling classes (each with its own setup/ teardown). Architecture (commands/install.py LOC): 1572 -> 1496. - Extracted F5 SSRF + F7 shell-metachar helpers to new dedicated module src/apm_cli/install/mcp_warnings.py (same pattern used in PR microsoft#809). - commands/install.py re-binds the extracted symbols at module scope (warn_ssrf_url -> _warn_ssrf_url, warn_shell_metachars -> _warn_shell_metachars, _is_internal_or_metadata_host, _SHELL_METACHAR_TOKENS, _METADATA_HOSTS) so existing test patches against apm_cli.commands.install._warn_* keep working unchanged. Tests: 4755/4755 unit + console pass (excludes the known pre-existing test_user_scope_skips_workspace_runtimes failure on main, unrelated to this PR or this merge). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR microsoft#809 (shell-string MCP command validation) merged to main after the previous merge commit on this branch. Re-merged to pick up the new tip. Conflicts: - src/apm_cli/install/mcp_warnings.py (add/add): both sides added this file. Took main's version -- it is the authoritative microsoft#809 copy and includes the FU4 extension (command= parameter on warn_shell_metachars) that my earlier manual extraction on this branch did not have. - src/apm_cli/commands/install.py: kept main's re-bind import ordering since the two versions were trivially equivalent. Tests: 4767/4767 unit + console pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel — Round 2 Verdict (PR #700)Re-review following arika (@arika0093)'s commit-by-item response mapping. Panel: Python Architect, CLI Logging, DevX UX, Supply-Chain Security, OSS Growth. Five specialists, full convergence, no disagreements to arbitrate. Verdict: APPROVE with minor nits. Ready to merge after one small fix. All 14 items from the previous round are substantively addressed. Evidence verified commit-by-commit. Items verified resolved
Architecture invariant ( Nits (should-fix; not merge-blockers)N1 — Double URL in validation-time path (CLI Logging, one-line fix) N2 — Wording inconsistency between warning and error copy (CLI Logging) N3 — Context-aware hint when only one gate is missing (DevX) N4 — N5 — Tracked follow-ups (out of scope; open as issues)
Growth / positioning (side-channel)PR #700 converts the README's "Install from anywhere" bullet from aspirational into defensible: corporate Gitea / Gogs / Artifactory / air-gapped Artifactory users can now install. The Contributor-velocity narrative: arika (@arika0093) (first-time external contributor, NONE-association) shipped a security-sensitive 1,300+ LOC feature with 204 new test functions, through two panel reviews, one atomic commit per review item. This is the contributor-funnel story APM should amplify in the release post — it sets the quality bar for future complex contributions. Recommended release-notes hook:
README "Install from anywhere" bullet should be updated in a follow-up to mention HTTP mirrors with explicit opt-in. Conflict resolutionMerged cleanly against
Full suite green on HEAD. No functional changes from these merge commits — pure conflict resolution. Final callMerge-ready. Single actionable ask before merge: fix N1 (one-line: swap Outstanding work by arika (@arika0093). Thank you for seeing this through two review rounds with the care each commit shows. Panel: python-architect, cli-logging-expert, devx-ux-expert, supply-chain-security-expert, oss-growth-hacker. Synthesis via apm-review-panel skill. Conflicts resolved in |
Round-2 apm-review-panel findings on PR microsoft#700. Five specialists converged on APPROVE with minor nits; this commit applies the actionable ones. N1 (cli-logging): fix double-URL rendering in the validation-time path. `_format_insecure_dependency_requirements` already embeds the full URL in the reason string, but `logger.validation_fail(package, reason)` prepends "{package} -- " and produced output like "[x] http://host/repo -- http://host/repo -- HTTP dependency...". Switch to `logger.error(reason)` on the insecure-reject branch to match the other two failure paths. `invalid_outcomes` still tracks the package so the later validation summary sees it. N2 (cli-logging): unify jargon register. Warning copy already says "(unencrypted)" post-review microsoft#11; align the error recipe from "(no transport encryption)" to "(unencrypted)" so both surfaces use the same wording. N3 (devx): context-aware remediation steps. `_check_insecure_dependencies` previously emitted the full two-step recipe in both failure branches, even when the user had already performed step 1 (manifest edit). The factory now takes `missing_dep_allow` and `missing_cli_flag` keyword args and only renders the missing step(s). The add-time validation path keeps the default of both-steps since the dep is not yet in apm.yml. Tests updated to match the new per-branch assertions. N4 (security): document the intentional two-stage credential-helper policy in `_build_noninteractive_git_env`. The pop-then-conditionally- restore of GIT_ASKPASS is correct but fragile; an explicit docstring prevents a future refactor from inverting the logic and leaking credentials over plaintext HTTP or blocking system keychains on HTTPS/SSH fallback. N5 (doc-writer): add `apm deps list --insecure` sample output to cli-commands.md showing the bold-red `Origin` column with `direct` and `via <parent>` example rows. F2 (doc-writer): document `is_insecure` and `allow_insecure` fields in lockfile-spec.md section 4.2, including replay semantics and legacy fail-closed behaviour. F3 (doc-writer): add "HTTP dependencies (opt-in)" section to the apm-guide dependencies.md skill resource so LLM agents consuming the skill have the dual-opt-in model, example manifest, example CLI invocation, and cross-references to commands.md and the enterprise security guide. Full unit suite: 4767/4767 passing (one deselect unrelated). `install.py` at 1497 LOC (under the 1525-LOC invariant). Panel verdict: microsoft#700 (comment 4290979672) Co-authored-by: arika0093 <arika0093@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Follow-up on the panel verdict: I've pushed Nits addressed
Follow-ups addressed (doc-writer agent)
Validation
Remaining follow-ups ( Thank you for the iteration quality -- the one-commit-per-review-item hygiene made this second round fast and high-confidence. |
CodeQL flagged `"http://gitlab.company.internal" in arg` at test_install_command.py:963 as a potentially unsafe URL substring check: the hostname could appear at an arbitrary position in the checked URL (e.g. inside a path segment, query value, or userinfo) rather than as the actual host, producing a false-positive match. Replace substring checks with `urllib.parse.urlsplit` and compare `scheme` and `netloc` explicitly: - test_explicit_http_generic_host_tries_http_first (the flagged case): parse each subprocess-arg URL and require scheme == "http" AND netloc == "gitlab.company.internal". - test_generic_host_falls_back_to_https_when_ssh_fails (sibling with the same pattern at line 929): same treatment for the HTTPS arg; keep the SSH SCP-form check as `"git@git.example.org:" in arg` since SCP-style URLs are not parseable by urlsplit. Both tests still assert the same contract: explicit `http://` does not fall back to SSH, and SSH failure falls back to HTTPS on the right host. Co-authored-by: arika0093 <arika0093@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
This PR adds explicit support for HTTP (insecure) APM dependencies behind an opt-in flow, and makes lockfile replays preserve that HTTP source information.
When adding a package, you can explicitly allow HTTP dependencies by providing the
--allow-insecureflag.When restoring packages, you also need to provide the
--allow-insecureflag unless the global config is enabled. If there are HTTP URLs in the dependency chain, it will error without--allow-insecureorapm config set allow-insecure true.The
apm.ymlfile will record it in the following format:HTTP dependencies require two explicit signals:
allow_insecure: trueon the dependency entry inapm.ymlapm install --allow-insecureorapm config set allow-insecure trueThe global configuration only removes the need to pass
--allow-insecureon the CLI. It does not remove theallow_insecure: truerequirement inapm.yml.Fixes #636
TODO
--allow-insecureflag when adding dependencies--allow-insecureflag when installing dependenciesallow_insecure: truefield inapm.ymland lockfileallow-insecureglobal configType of change
Testing