Skip to content
Merged
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
59 changes: 33 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ attach-guard is a Claude Code plugin that intercepts package installation comman

- Installs as a Claude Code plugin — no manual hook configuration needed
- Intercepts `npm install`, `pnpm add`, `pip install`, `go get`, and `cargo add` commands via PreToolUse hooks
- Checks package scores, age, and alerts via Socket.dev
- Denies known malware and low-score packages automatically
- Asks for confirmation on gray-band packages
- Evaluates packages with the configured provider before install commands run
- Uses the current Socket.dev adapter only as an explicit bring-your-own-token local provider, not a hosted/default scoring source
- Denies known malware and high-confidence dangerous packages automatically
- Asks for confirmation on gray-band packages and provider-unavailable cases in local mode
- Rewrites unpinned installs to safe pinned versions when possible
- Fails closed when the provider is unavailable

Attach Open Score is the first-party provider direction and is not wired in yet; see [Attach Open Score provider semantics](docs/OPEN_SCORE_PROVIDER.md) for the planned integration contract.
- Logs every decision to a local JSONL audit trail

## Smart Version Replacement: Block Without Breaking Flow
Expand All @@ -27,20 +29,20 @@ Most security tools just say "no." attach-guard says "no, but here's a safe alte

When a risky version is blocked, attach-guard finds the newest version that passes policy and offers it as a replacement. Claude sees the safe alternative and can proceed immediately — your flow doesn't stop, it gets redirected to a safe path.

**npm** — axios v1.14.1 and v0.30.4 were [compromised versions](https://socket.dev/blog/axios-npm-account-compromise) published via a hijacked maintainer account:
**npm** — axios v1.14.1 and v0.30.4 were publicly reported compromised versions published via a hijacked maintainer account:

```
> npm install axios

attach-guard evaluates:
axios@1.14.1 --> DENY (supply chain score 40, below threshold 50 — compromised version)
axios@1.14.0 --> ALLOW (supply chain score 71, passes all policy checks)
axios@1.14.1 --> DENY (known compromised version)
axios@1.14.0 --> ALLOW (passes configured policy checks)

Result: ASK + rewritten command
"npm install axios@1.14.0"
```

**pip** — litellm v1.82.7 and v1.82.8 were [malicious versions](https://socket.dev/npm/package/litellm) published to PyPI:
**pip** — litellm v1.82.7 and v1.82.8 were malicious versions published to PyPI:

```
> pip install litellm
Expand All @@ -53,7 +55,7 @@ Result: ASK + rewritten command
"pip install litellm==1.82.6"
```

These are real examples — attach-guard blocks compromised versions automatically based on their supply chain scores.
These examples illustrate the current enforcement flow: attach-guard blocks known compromised or policy-failing versions and offers a safe pinned alternative when one is available.

| Scenario | Example | Decision | What happens |
|---|---|---|---|
Expand Down Expand Up @@ -87,7 +89,7 @@ Security enforcement requires interception at the tool-call boundary, before exe

### Quick Start: Claude Code Plugin

The fastest way to try attach-guard. Requires a [Socket.dev](https://socket.dev) API token (free tier available).
The fastest way to try the current packaged plugin. Today's released plugin uses the local bring-your-own-token Socket.dev provider for real scoring. Attach Open Score is the first-party direction and is not wired in yet; Socket must not be treated as hosted/default Attach scoring.

```bash
# Add the marketplace and install (one-time)
Expand All @@ -101,7 +103,7 @@ Or from within a Claude Code session:
/plugin install attach-guard@attach-dev
```

During installation or enablement, Claude Code will prompt for your Socket API token (stored securely in your system keychain). Get a free token at [socket.dev](https://socket.dev).
During installation or enablement, the current Socket-backed plugin path prompts for your Socket API token (stored securely in your system keychain). Get a free token at [socket.dev](https://socket.dev). This token is for local BYO-provider use only.

> **If the install/enable prompt didn't appear**, re-trigger it with:
> ```bash
Expand Down Expand Up @@ -136,7 +138,7 @@ For use without the plugin system, or to install the binary globally.
#### Prerequisites

- [Go 1.21+](https://go.dev/dl/) (to build from source; not needed for the plugin install above)
- A [Socket.dev](https://socket.dev) API token (free tier available)
- A [Socket.dev](https://socket.dev) API token only when using the current Socket BYO-token provider path

#### Step 1: Build and install the binary

Expand Down Expand Up @@ -164,7 +166,7 @@ attach-guard version
# attach-guard v0.1.0
```

#### Step 2: Set up your Socket API token
#### Step 2: Set up a Socket API token for the current BYO-provider path

```bash
export SOCKET_API_TOKEN="your-token-here"
Expand Down Expand Up @@ -213,7 +215,7 @@ Try installing a known-compromised version to verify attach-guard blocks it:
> Install axios@1.14.1

Claude: I'll install axios@1.14.1.
[attach-guard] deny: axios@1.14.1: supply chain score 40 is below minimum threshold 50
[attach-guard] deny: axios@1.14.1: known compromised version
```

Then try a safe version:
Expand Down Expand Up @@ -275,9 +277,11 @@ attach-guard hook

Default config location: `~/.attach-guard/config.yaml`

Current provider configuration still defaults to the Socket adapter in code. Treat this as a legacy/local BYO-token default until the Attach Open Score provider lands; do not use Socket as hosted/default Attach scoring.

```yaml
provider:
kind: socket # risk intelligence provider
kind: socket # explicit BYO-token local provider today; Open Score is the first-party direction
api_token_env: SOCKET_API_TOKEN
policy:
deny_known_malware: true
Expand Down Expand Up @@ -334,12 +338,14 @@ Highest priority wins (later sources override earlier):
### Unpinned version handling

When you run an unpinned supported command such as `npm install axios`, `pip install requests`, `go get golang.org/x/net`, or `cargo add serde`:
- attach-guard fetches candidate versions from the matching registry and scores them via Socket.dev
- attach-guard fetches candidate versions from the configured provider and evaluates them against policy
- If the latest passes policy, the command runs as-is
- If the latest fails but an older version passes, attach-guard suggests a rewrite using ecosystem-native syntax
- In Claude Code mode: returns `ask` with the rewritten command via `updatedInput`
- If no version passes, denies

Attach Open Score integration should be verdict-first: `ALLOW` → allow, `ASK` → ask, `DENY` → deny, and `UNKNOWN` → ask/warn locally by default. CI/team policy may map unknowns to deny by explicit configuration. See [Attach Open Score provider semantics](docs/OPEN_SCORE_PROVIDER.md).

### Failure handling

- Local/interactive mode: asks on provider failure
Expand All @@ -356,18 +362,18 @@ Every decision is logged to `~/.attach-guard/audit.jsonl`:
"user": "dev",
"cwd": "/home/dev/project",
"package_manager": "npm",
"original_command": "npm install axios@1.14.1",
"original_command": "npm install example-malware@1.0.0",
"decision": "deny",
"reason": "axios@1.14.1: supply chain score 40 is below minimum threshold 50",
"packages": [{"ecosystem":"npm","name":"axios","selected_version":"1.14.1","score":{"supply_chain":40,"overall":40}}],
"provider": "socket",
"reason": "example-malware@1.0.0: known malware alert",
"packages": [{"ecosystem":"npm","name":"example-malware","selected_version":"1.0.0","score":{"supply_chain":0,"overall":0},"alerts":[{"severity":"critical","title":"known malware","category":"malware"}]}],
"provider": "mock",
"mode": "claude"
}
```

## API Quota
## Socket BYO-token provider quota

attach-guard uses the [Socket.dev API](https://socket.dev) for package risk scoring. The free tier provides **500 quota units per hour**.
The current Socket adapter uses the [Socket.dev API](https://socket.dev) when the user explicitly configures the BYO-token Socket provider. This section is for that local provider path only; Socket data must not be used as a default Attach Open Score input/source absent an explicit partnership.

| Ecosystem | Endpoint | Cost per call |
|---|---|---|
Expand All @@ -380,9 +386,9 @@ attach-guard uses the [Socket.dev API](https://socket.dev) for package risk scor
- Pinned installs (e.g. `pip install litellm==1.82.8`) use one call to score a single version
- Unpinned installs (e.g. `pip install litellm`) use one batch call to score up to 10 candidate versions

**When quota is exhausted**, scoring calls fail and attach-guard falls back to zero scores. This means:
- Pinned installs are **denied** (score 0 < threshold 50) — safe, fails closed
- Unpinned installs show "no acceptable version found" instead of offering a safe alternative — the version rewrite feature requires real scores to identify which version passes policy
**When quota is exhausted**, provider calls fail. In current behavior this means:
- Pinned installs are **denied** because the provider could not return an acceptable evaluation — safe, fails closed
- Unpinned installs show "no acceptable version found" instead of offering a safe alternative — the version rewrite feature requires real provider results to identify which version passes policy

To check your remaining quota:
```bash
Expand All @@ -395,7 +401,8 @@ Quota resets hourly. For higher limits, see [Socket.dev pricing](https://socket.

- Direct `pip` / `pip3` (including `uv pip`), `go get` / `go install`, and `cargo add` / `cargo install` are supported; `python -m pip` remains passthrough for now
- pip extras/range specs, Cargo requirement syntax, and non-semver Go queries are intentionally passed through for manual review rather than being auto-evaluated
- PyPI, Go, and Cargo scoring uses Socket's `POST /v0/purl` endpoint which has higher quota cost (100 units) compared to npm (10 units)
- Current released provider implementation is Socket BYO-token/local; Attach Open Score first-party provider integration is the next direction
- PyPI, Go, and Cargo scoring through the Socket BYO provider uses Socket's `POST /v0/purl` endpoint, which has higher quota cost (100 units) compared to npm (10 units)
- No transitive dependency analysis
- No lockfile graph support
- Single provider at a time
Expand Down
9 changes: 7 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ raw command string
┌──────────┐
│ Provider │ ── Socket adapter fetches scores, versions, alerts
│ Provider │ ── scores, versions, alerts today; future Open Score integration maps verdicts
└────┬─────┘
Expand Down Expand Up @@ -71,7 +71,12 @@ type Provider interface {
}
```

The Socket adapter normalizes Socket API responses into internal `VersionInfo` and `PackageScore` types. No Socket-specific types leak into the policy engine.
Current implementation status:
- the shipped adapter is Socket-backed and should be treated as an explicit local BYO-token provider
- the first-party default direction is Attach Open Score; see [`docs/OPEN_SCORE_PROVIDER.md`](OPEN_SCORE_PROVIDER.md) for verdict mapping and source/legal constraints
- no proprietary provider types should leak into the policy engine

The current Socket adapter normalizes Socket API responses into internal `VersionInfo` and `PackageScore` types. Future Open Score integration should avoid directly mapping Open Score's risk score into the existing safety-score fields because the polarity is opposite; use verdict-first mapping or an explicit transform with tests.

To add a new provider:
1. Implement the `Provider` interface
Expand Down
155 changes: 155 additions & 0 deletions docs/OPEN_SCORE_PROVIDER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Attach Open Score provider semantics

Status: design note for the attach-guard integration path
Audience: attach-guard maintainers, Attach Open Score implementers, policy authors

## Goal

attach-guard should treat Attach Open Score as the first-party default scoring direction while keeping proprietary providers, including Socket, as bring-your-own-token local integrations unless an explicit partnership permits broader hosted/default use.

This note defines how Attach Open Score verdicts should map into attach-guard behavior before code is added.

## Source and licensing posture

Allowed default Attach Open Score inputs are public, open, or otherwise terms-permitted sources with attribution and source references, including OSV, GitHub Advisory Database, deps.dev, OpenSSF Scorecard, public registry metadata, and package artifacts where allowed by each source's license/terms.

Forbidden for default or hosted Attach scoring unless explicitly reviewed and permitted:

- copying, scraping, reselling, or redistributing Socket/Snyk/Aikido/Sonatype/Endor scores or vendor data
- using proprietary vendor scores as calibration labels, training data, fixtures, public examples, or threshold targets
- exposing a paid API that behaves like a raw upstream dataset redistribution service

Socket can remain useful as a local BYO-token provider, but it must not be framed as the default Attach scoring source.

## Decision mapping

Attach Open Score v0 emits uppercase public decisions:

| Attach Open Score | attach-guard behavior | Local default | CI/team default |
|---|---|---|---|
| `ALLOW` | allow install | allow | allow |
| `ASK` | require review / user confirmation | ask/warn | configurable; often deny or require policy approval |
| `DENY` | block install | deny | deny |
| `UNKNOWN` | insufficient evidence | ask/warn | configurable; often deny or require policy approval |

attach-guard currently has internal `allow`, `ask`, and `deny` decisions only. Until `unknown` becomes a first-class internal decision, Open Score `UNKNOWN` should map to `ask` at the provider/policy boundary for local mode. CI/team policy may map `UNKNOWN` to deny by explicit configuration.

## Integration boundary

The preferred v0 implementation should avoid forcing Open Score through the existing Socket-style `PackageScore` threshold path. Add an explicit verdict-carrying result at the provider/policy boundary, for example:

```go
type ProviderVerdict struct {
Decision string // ALLOW, ASK, DENY, UNKNOWN
RiskScore *int // Open Score risk score, higher means riskier
Reasons []string // Open Score reason codes or rendered reason IDs
SourceRefs []string // source reference IDs/URLs safe for audit output
}
```

For the next implementation pass, use option 1: extend the provider/policy result shape so policy can consume `ProviderVerdict` directly. Legacy Socket score thresholds should remain provider-specific signals, not the generic contract for Open Score.

Decision precedence should remain conservative:

- explicit local/team denylist beats provider `ALLOW`
- known malware or high-confidence critical evidence beats provider `ALLOW`
- provider `DENY` blocks unless an explicit allowlist/policy override exists
- provider `ASK` and `UNKNOWN` require confirmation locally and may fail policy in CI/team mode
- provider unavailability maps to local ask/warn by default, not silent allow

## Score polarity warning

Attach Open Score's numeric `score` is a risk score: higher means riskier.

attach-guard's current `PackageScore.SupplyChain` and `PackageScore.Overall` fields are treated as safety-ish scores: lower means worse, and the current policy denies when `supply_chain < threshold`.

Therefore, do not map Open Score `score` directly into `PackageScore.SupplyChain` or `Overall`. That would invert behavior.

Acceptable implementation patterns:

1. **Verdict-first bridge** — map Open Score `decision` directly to attach-guard allow/ask/deny behavior, and preserve score/reasons/source refs for explanation/audit UX.
2. **Explicit score transform** — if existing threshold code must be reused temporarily, transform `safety_score = 100 - risk_score` and add tests proving polarity for ALLOW/ASK/DENY/UNKNOWN fixtures.
3. **Policy refactor** — make attach-guard policy understand decision-first verdicts and keep risk score as supporting context rather than the primary decision variable.

The preferred v0 integration is verdict-first. This leaves less room for accidental polarity inversions.

## Reason and source propagation

Open Score verdicts include reason codes and `source_refs`. attach-guard should preserve these in user-facing reasons and audit logs where possible.

Initial implementation can compress reasons into a human-readable reason string, but should avoid discarding structured data permanently. Future audit entries should be able to include:

- Open Score reason codes
- severity / decision effect
- source reference IDs
- source names and URLs where safe
- evaluation timestamp and TTL

## Provider availability

Local developer mode should not default to fail-closed on provider unavailability or unknown evidence. Default local behavior:

```text
provider unavailable → ASK / warn
UNKNOWN verdict → ASK / warn
```

CI/team mode can be stricter by explicit configuration:

```text
provider unavailable → DENY or policy failure
UNKNOWN verdict → DENY or policy failure
```

## Config direction

Current config supports a single provider kind and an environment override:

```yaml
provider:
kind: socket
```

```bash
ATTACH_GUARD_PROVIDER=mock
```

Future provider kind for this integration should be `open-score`.

```yaml
provider:
kind: open-score
endpoint: http://127.0.0.1:8757 # local or hosted Attach Open Score-compatible HTTP endpoint
timeout_seconds: 5
policy:
unknown_behavior:
local: ask # ask | deny | allow
ci: deny
provider_unavailable_behavior:
local: ask # ask | deny | allow
ci: deny
```

The v0 implementation target is an HTTP client provider. Embedded Go package or external CLI modes can be added later, but should not block the first provider pass.

Socket provider docs should show explicit opt-in:

```yaml
provider:
kind: socket
api_token_env: SOCKET_API_TOKEN
```

## Implementation checklist

Before adding code:

- [x] Provider kind for the next pass: `open-score`.
- [x] Runtime shape for the next pass: HTTP client provider against a local or hosted Attach Open Score-compatible endpoint.
- [ ] Extend provider/policy result shape with a verdict-first result such as `ProviderVerdict`.
- [ ] Add fixture-driven tests using public-safe synthetic verdicts.
- [ ] Test `UNKNOWN` mapping in local and CI modes via `policy.unknown_behavior`.
- [ ] Test provider-unavailable behavior via `policy.provider_unavailable_behavior`.
- [ ] Test score polarity so high-risk scores cannot accidentally become high-safety scores.
- [ ] Preserve source/legal attribution in docs and audit output.
- [ ] Keep Socket as explicit BYO-token/local provider.