# OSSF Scorecard Compliance Notes

This document explains our compliance status with OSSF Scorecard checks
and documents any accepted risks or false positives.

## Current Score: 8/10 for Pinned-Dependencies

**Summary:**
- ✅ 196/196 GitHub Actions pinned by SHA
- ✅ 4/4 Container images pinned by digest
- ✅ 1/1 Security tools use official SHA-pinned actions (zizmor)
- ⚠️ 2/27 pip commands pinned by hash (remainder version-pinned — accepted risk)
- ⚠️ 21/24 npm commands pinned (3 are operational commands)
- ⚠️ 1 false positive for downloadThenRun
- ⚠️ APT packages intentionally unpinned (base image controls versions)

## Pinned-Dependencies

### GitHub Actions: COMPLIANT ✅

All GitHub Actions use commit SHA pinning (40-character hex):

```yaml
# Examples from our workflows:
actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3  # v6.0.0
actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548  # v6
step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2  # v2.13.3
```

### Docker Images: COMPLIANT ✅

All Docker images use SHA256 digest pinning:

```yaml
# Examples from our workflows and docker-compose:
python:3.13.9-slim@sha256:326df678c20c78d465db501563f3492d17c42a4afe33a1f2bf5406a1d56b0e86
redis:alpine@sha256:8360960f5fb56a282d78686203dd875862cd4b52a4184c17ac753690252d6d31
node:20-alpine@sha256:bcd88137d802e2482c9df3cdec71e0431857ebbbdba6973776b5593214056d86
```

### Official GitHub Actions: COMPLIANT ✅

One security tool uses an official GitHub Action with SHA-pinning,
which provides equivalent security to hash-pinned pip installs:

| Tool | Action | SHA |
|------|--------|-----|
| zizmor | `zizmorcore/zizmor-action` | `135698455da5c3b3e55f73f4419e481ab68cdd95` |

This official action runs the tool in a container with internal integrity verification,
which OSSF Scorecard accepts as equivalent to hash pinning.

> **Note:** pip-audit was removed because it duplicates OSV-Scanner's coverage (both
> query the OSV database) and its internal use of pip's dependency resolver conflicts
> with PDM's `[tool.pdm.resolution.overrides]`, causing persistent CI failures.

> **Note:** Checkov previously used `bridgecrewio/checkov-action` but was reverted to CLI
> installation (`pip install checkov==3.2.499`) due to a known bug with multiple
> consecutive action calls. See [checkov-action#170](https://github.com/bridgecrewio/checkov-action/issues/170)
> and [checkov#5866](https://github.com/bridgecrewio/checkov/issues/5866).

### pip install: VERSION-PINNED (Accepted Risk) ⚠️

Scorecard flags `pip install package==version` because it prefers hash pinning.
The remaining pip commands use exact version pinning.

**Flagged commands and their status:**

| File | Line | Command | Status |
|------|------|---------|--------|
| Dockerfile | 49-50 | `pip3 install pip==26.0` `pip install pdm==2.26.2...` | Version-pinned |
| Dockerfile | 194 | `pip3 install pip==26.0` | Version-pinned |
| check-env-vars.yml | 36 | `pip install loguru==0.7.3 sqlalchemy==2.0.36...` | Version-pinned |
| checkov.yml | 33 | `pip install checkov==3.2.499` | Version-pinned |
| fuzz.yml | 51-52 | `pip install pip==25.0` `pip install pdm==2.26.2` | Version-pinned |
| mypy-type-check.yml | 39,47-48 | `pip install pdm==2.26.2 mypy==1.14.1...` | Version-pinned |
| publish.yml | 139,328 | `pip install pdm==2.26.2` `pip install wheel==0.46.2` | Version-pinned |
| puppeteer-e2e-tests.yml | 65 | `pip install -e .` | Local package |
| responsive-ui-tests-enhanced.yml | 97-98 | `pip install pip==25.0` `pip install -e .` | Version-pinned + local (Alert #5688) |
| semgrep.yml | 36 | `pip install semgrep==1.87.0` | Version-pinned |
| update-precommit-hooks.yml | 35-38 | `pip install pip==25.0` `pip install pre-commit-update==0.6.1` | **Hash-pinned** |
| validate-image-pinning.yml | 67 | `pip install pyyaml==6.0.2` | Version-pinned |
| backwards-compatibility.yml | 66-68 | `pip install --upgrade pip` `pip install pytest` `pip install -e .` | Intentionally unpinned |
| backwards-compatibility.yml | 106-107 | `pip install --upgrade pip` `pip install pytest` | Intentionally unpinned |
| backwards-compatibility.yml | 154-155 | `pip install --upgrade pip` `pip install "local-deep-research==..."` | Intentionally unpinned |
| backwards-compatibility.yml | 169 | `pip install -e .` | Local package |

> **Note:** `backwards-compatibility.yml` intentionally uses unpinned pip commands
> because it tests compatibility with prior PyPI releases of local-deep-research.
> Pinning these commands would defeat the purpose of the compatibility tests.

**Why we don't use hash pinning:**

1. **Platform-specific hashes**: pip package hashes vary by Python version, OS, and architecture.
   A single hash won't work across different CI runners.

2. **Maintenance burden**: Every version update requires regenerating hashes for all platforms.

3. **Marginal security benefit**: These are dev/CI tools running in hardened CI environments
   (step-security/harden-runner) with egress auditing. Supply chain attacks on PyPI packages
   are mitigated by version pinning and short execution windows.

4. **Industry practice**: Version pinning (`==`) is the standard for CI tool installation.
   Hash pinning is typically reserved for production dependencies.

5. **Local packages**: `pip install -e .` installs the local source code and cannot be hash-pinned.

6. **Bootstrap commands**: `pip install pip==X.Y.Z` cannot be hash-pinned because pip is the
   tool performing the verification — it cannot verify its own integrity during a self-upgrade.

### Specific OSSF Scorecard Alerts

| Alert | File | Lines | Description | Status |
|-------|------|-------|-------------|--------|
| #5688 | responsive-ui-tests-enhanced.yml | 97-98 | `pip install pip==25.0` (bootstrap) and `pip install -e .` (local source) | Accepted limitation |

### npm Commands: MOSTLY COMPLIANT (21/24) ⚠️

The 3 "unpinned" npm commands are operational commands, not package installations:

| File | Line | Command | Reason Not Pinned |
|------|------|---------|-------------------|
| npm-audit.yml | 56 | `npm i --package-lock-only` | Generates lockfile only |
| npm-audit.yml | 72 | `npm i --package-lock-only` | Generates lockfile only |
| update-npm-dependencies.yml | 74 | `npm update` | Intentionally updates to latest |

These commands don't install packages directly - they either generate lockfiles
or intentionally update packages. They cannot and should not be "pinned".

### downloadThenRun: FALSE POSITIVE ⚠️

**Flagged:** `examples/elasticsearch/test_elasticsearch.sh:60`

```bash
curl -s http://localhost:9200 | python3 -m json.tool | head -10
```

**Why it's a false positive:**
- This fetches JSON from localhost:9200 (local Elasticsearch)
- Pipes to `python3 -m json.tool` (stdlib JSON formatter)
- Shows first 10 lines of pretty-printed output

This is NOT downloading and running a remote script. It's formatting local JSON output.
The scorecard pattern-matches `curl | python` as potentially dangerous, but this
is a safe operation on localhost data.

**OSSF Scorecard Alert:** #4411

### APT Packages: INTENTIONALLY UNPINNED ⚠️

**Files affected:** `publish.yml`, `e2e-research-test.yml`, `responsive-ui-tests-enhanced.yml`, `Dockerfile`

| File | Packages | Runner/Base |
|------|----------|-------------|
| publish.yml | libsqlcipher-dev, patchelf | ubuntu-22.04 |
| e2e-research-test.yml | jq | ubuntu-22.04 |
| responsive-ui-tests-enhanced.yml | wget, gnupg, ca-certificates, fonts-liberation, etc. | ubuntu-latest |
| Dockerfile | curl, git, build-essential, etc. | python:3.13.9-slim@sha256:... |

**Rationale for NOT pinning APT packages:**

1. **Version availability**: Old APT package versions are removed from Ubuntu archives after 6-12 months.
   Pinning to `package=1.2.3-1ubuntu1` causes builds to fail when that version is removed.

2. **Base image controls versions**: Docker base images are SHA-pinned, which deterministically controls
   which APT package versions are available. The combination of `python:3.13.9-slim@sha256:326df678...`
   and `apt-get install curl` produces the same result every time that base image is used.

3. **Runner stability**: GitHub workflow runners use pinned Ubuntu versions (e.g., `ubuntu-22.04`)
   which provide consistent package versions throughout the runner's lifecycle.

4. **Version variation**: APT package version strings vary between Ubuntu releases and architectures,
   making cross-platform pinning impractical.

5. **Industry consensus**: Security experts recommend pinning the base image/runner rather than
   individual packages. Base image pinning provides stronger guarantees with lower maintenance burden.

**Mitigations in place:**

- ✅ Docker base images pinned to SHA256 digests (see Docker Images section above)
- ✅ GitHub runner versions pinned where practical (ubuntu-22.04)
- ✅ Dependabot configured to monitor for security updates
- ✅ Step-security/harden-runner audits all egress traffic
- ✅ Minimal package sets installed (only what's needed)

### Enforcement

We have automated verification for our pinning strategy:
- `.github/workflows/validate-image-pinning.yml` - Validates Docker image digests
- Pre-commit hooks verify action SHA pinning
- All pip install commands use explicit version specifiers (except `backwards-compatibility.yml` which intentionally tests with unpinned versions)

### Review Cadence

These decisions are reviewed quarterly to ensure they remain appropriate:
- **Next review:** Q2 2026
- **Owner:** Security team

## References

- [OSSF Scorecard Pinned-Dependencies Check](https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies)
- [StepSecurity Harden Runner](https://github.com/step-security/harden-runner)
- [pip Hash Checking Mode](https://pip.pypa.io/en/stable/topics/secure-installs/#hash-checking-mode)
