# dependency_scanner_tool

Scans Python and Node.js dependencies for known vulnerabilities, outdated packages, and license policy violations.  
Integrates as **Gate 3** in `release_check`.

---

## Purpose

| Concern | Source A | Source B |
|---|---|---|
| **Vulnerabilities** | OSV.dev database (online or cached) | Pinned deps from lock files |
| **Outdated packages** | Fixed versions in OSV findings | Current versions in lock files |
| **License policy** | Configured deny/warn list | Package metadata (limited in MVP) |

---

## RBAC

| Entitlement | Grants |
|---|---|
| `tools.deps.read` | Run scan (agent_cto, agent_oncall) |
| `tools.deps.gate` | Gate execution in release_check (agent_cto only) |

---

## Limits (`config/tool_limits.yml`)

| Param | Value |
|---|---|
| `timeout_ms` | 45 000 ms |
| `max_chars_in` | 3 000 |
| `max_bytes_out` | 1 048 576 (1 MB) |
| `rate_limit_rpm` | 5 |
| `concurrency` | 1 |

---

## Invocation

```json
{
  "tool": "dependency_scanner_tool",
  "action": "scan",
  "targets": ["python", "node"],
  "vuln_mode": "offline_cache",
  "fail_on": ["CRITICAL", "HIGH"],
  "timeout_sec": 40
}
```

### Parameters

| Param | Type | Default | Description |
|---|---|---|---|
| `action` | string | — | Must be `"scan"` |
| `targets` | array | `["python","node"]` | Ecosystems to scan |
| `vuln_mode` | string | `"offline_cache"` | `"online"` queries api.osv.dev; `"offline_cache"` uses local cache only |
| `fail_on` | array | `["CRITICAL","HIGH"]` | Severity levels that block release |
| `timeout_sec` | number | `40` | Hard wall-clock timeout |

---

## Response

```json
{
  "pass": true,
  "summary": "✅ Dependency scan PASSED. 120 deps scanned, 0 vulns found.",
  "stats": {
    "ecosystems": ["PyPI", "npm"],
    "files_scanned": 4,
    "deps_total": 120,
    "deps_pinned": 115,
    "deps_unresolved": 3,
    "vulns_total": 0,
    "by_severity": {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0},
    "outdated_total": 0
  },
  "vulnerabilities": [],
  "outdated": [],
  "licenses": [],
  "recommendations": []
}
```

### Vulnerability object

```json
{
  "id": "GHSA-35jh-r3h4-6jhm",
  "ecosystem": "npm",
  "package": "lodash",
  "version": "4.17.20",
  "severity": "HIGH",
  "fixed_versions": ["4.17.21"],
  "aliases": ["CVE-2021-23337"],
  "evidence": {"file": "services/render-pptx-worker/package-lock.json", "details": "lodash==4.17.20"},
  "recommendation": "Upgrade lodash from 4.17.20 to 4.17.21"
}
```

---

## Pass / Fail Rule

| Condition | Result |
|---|---|
| Any `CRITICAL` or `HIGH` vuln found | `pass=false` (gate blocks) |
| Any denied license found | `pass=false` |
| `MEDIUM` vulns only | `pass=true`, added to recommendations |
| `UNKNOWN` severity (cache miss) | `pass=true`, recommendation to populate cache |

---

## Supported Manifest Files

### Python (priority order)
1. `poetry.lock` — fully resolved versions
2. `Pipfile.lock` — resolved versions
3. `requirements*.txt` — only `==` pinned lines are scanned; unpinned noted
4. `pyproject.toml` — declared deps listed (no version resolution)

### Node.js (priority order)
1. `package-lock.json` (npm v2/v3)
2. `pnpm-lock.yaml`
3. `yarn.lock`
4. `package.json` — only if no lock file present

---

## Vulnerability Sources

### OSV.dev

**Online mode** (`vuln_mode=online`):
- Queries `https://api.osv.dev/v1/querybatch` in batches of 100
- Requires entry in `config/network_allowlist.yml` (`dependency_scanner_tool.hosts: api.osv.dev`)
- New results are cached to `ops/cache/osv_cache.json`

**Offline cache mode** (`vuln_mode=offline_cache`, default):
- Reads from `ops/cache/osv_cache.json` only
- Cache misses → severity `UNKNOWN` (not blocking by default)
- No outbound network calls

**Cache format** (`ops/cache/osv_cache.json`):
```json
{
  "version": 1,
  "updated_at": "...",
  "entries": {
    "PyPI:requests:2.31.0": {"vulns": [], "cached_at": "..."},
    "npm:lodash:4.17.20": {"vulns": [...], "cached_at": "..."}
  }
}
```
Cache key: `{ecosystem}:{normalized_name}:{version}`

---

## Security

- **Read-only**: scans lock files; no writes (except optional cache update in online mode)
- **Evidence redaction**: secrets/tokens masked before inclusion in report
- **No payload logging**: only hash of dep list + counts logged to audit trail
- **Path traversal protection**: excluded dirs (`node_modules`, `.git`, `.venv`, etc.)
- **Size limits**: max 80 files, 2000 deps, 500 vulns enforced in code

---

## Integration in release_check

Gate order: `pr_review` → `config_lint` → **`dependency_scan`** → `contract_diff` → `threat_model` → `smoke` → `drift`

`release_check` inputs related to this gate:

| Input | Type | Default | Description |
|---|---|---|---|
| `run_deps` | boolean | `true` | Enable dependency scan gate |
| `deps_targets` | array | `["python","node"]` | Ecosystems |
| `deps_vuln_mode` | string | `"offline_cache"` | OSV mode |
| `deps_fail_on` | array | `["CRITICAL","HIGH"]` | Blocking severity |
| `deps_timeout_sec` | number | `40` | Timeout |

---

## Outdated Analysis (lockfile_only mode)

In MVP, "latest version" is inferred from OSV `fixed_versions` only (no registry lookup).
An upgrade is recommended if a fixed version > current version exists in an OSV finding.

Full latest-version lookup (PyPI/npm registry) is planned as an optional enhancement.

---

## Extending the Cache

To refresh the offline cache:
1. Set `vuln_mode: online` in a controlled environment with outbound access to `api.osv.dev`
2. Run `dependency_scanner_tool` — new entries are merged into `ops/cache/osv_cache.json`
3. Commit the updated cache file

Or use `ops/scripts/refresh_osv_cache.py` (planned).
