Skip to content
Closed
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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,38 @@ the [OpenSSF Scorecard
documentation](https://github.com/ossf/scorecard/blob/main/docs/checks.md)
for more information on each check.

#### SARIF Upload

The Scorecard policy can optionally upload results as
[SARIF](https://sarifweb.azurewebsites.net/) to each repository's
**Security > Code Scanning** tab. This gives organization administrators
visibility into Scorecard findings alongside other security tools (CodeQL,
Dependabot, etc.) without requiring per-repository workflow setup.

To enable SARIF upload, add the `upload` field to your `scorecard.yaml`:

```yaml
optConfig:
optOutStrategy: true
action: issue
checks:
- Binary-Artifacts
- Signed-Releases
threshold: 8
upload:
sarif: true
```

**Requirements:**
- The Allstar GitHub App must have the **Code scanning alerts** repository
permission set to **Read & write** (API scope: `security_events`).
Self-hosted operators need to add this permission to their GitHub App. The
public Allstar App operated by OpenSSF does not yet include this permission.
- SARIF upload is non-blocking: if the upload fails (e.g., due to missing
permissions), the policy check continues normally.
- Change detection compares the repository HEAD commit SHA and skips the scan
and upload when the repo has not been pushed to since the last upload.

### GitHub Actions

This policy's config file is named `actions.yaml`, and the [config definitions
Expand Down
218 changes: 218 additions & 0 deletions openspec/changes/evidence-upload/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# Design: Evidence Upload for Allstar Scorecard Policy

Companion to [proposal.md](proposal.md). This document captures the technical
design decisions.

## Configuration model

**Decision:** `upload: {sarif: true}` nested under the Scorecard policy config
(`scorecard.yaml`).

```yaml
# .allstar/scorecard.yaml
optConfig:
optOutStrategy: true
action: issue
checks:
- Binary-Artifacts
- Signed-Releases
threshold: 8
upload:
sarif: true
```

**Rationale:**

- Nested `upload` struct is extensible for v6 formats (`intoto`, `oscal`)
without polluting the top-level config
- Scorecard policy level (not top-level `allstar.yaml`) keeps the feature
scoped while testing
- Boolean per-format allows independent enable/disable
- Follows Allstar's existing flat + nested config patterns

**Go types:**

```go
type UploadConfig struct {
SARIF bool `json:"sarif"`
}

// OrgConfig (concrete value with defaults):
Upload UploadConfig `json:"upload"`

// RepoConfig (pointer for "was this set?" override semantics):
Upload *UploadConfig `json:"upload,omitempty"`
```

**Alternatives considered:**

| Option | Rejected because |
|--------|-----------------|
| `sarifUpload: true` (flat boolean) | Not extensible for v6 formats |
| `action: [issue, sarif]` (multi-action array) | Requires refactoring Policy interface across all 9 policies |
| `upload: {enabled: true, format: sarif, ...}` (nested with format key) | Over-engineered before need is proven |

## Execution model

**Decision:** Dual execution — keep the per-check loop for issue text
generation, add a second full `sc.Run()` call for SARIF when upload is enabled.

**Rationale:**

The current Scorecard policy runs each check individually in a loop
(`scorecard.go:159-239`), building `NotifyText` for GitHub issues. It `break`s
on first error, yielding partial results. This model cannot produce valid SARIF
because `Result.AsSARIF()` needs a complete `scorecard.Result` from a single
`sc.Run()` call with all checks.

Refactoring the per-check loop to a single `Run()` would change error handling
semantics for the existing issue action. Dual execution preserves existing
behavior while adding SARIF capability.

**Flow:**

```
Check() {
// Path 1: existing per-check loop (preserved)
for _, check := range mc.Checks {
result := scRun(repo, []string{check})
// build NotifyText for issues
}

// Path 2: full run for SARIF (new, opt-in)
if mc.Upload.SARIF {
fullResult := scRun(repo, mc.Checks) // all checks at once
sarif := fullResult.AsSARIF(...)
uploadSARIF(sarif)
}
}
```

**Cost:** One additional Scorecard scan per repo when upload is enabled. This
is acceptable because:
- Upload is opt-in
- Change detection skips the scan and upload when the commit SHA hasn't changed
- Allstar already scans every 5 minutes; the additional scan adds ~seconds

## SARIF generation

**Decision:** Use `scorecard/v5.Result.AsSARIF()` (already available as a
direct dependency at v5.4.0).

**AsSARIF() signature:**

```go
func (r *Result) AsSARIF(
showDetails bool,
logLevel log.Level,
writer io.Writer,
checkDocs docs.Doc,
policy *spol.ScorecardPolicy,
opts *options.Options,
) error
```

**Parameters** (pattern from scorecard-action `format.go`):

- `showDetails` = `true`
- `logLevel` = `sclog.DefaultLevel`
- `writer` = `bytes.Buffer`
- `checkDocs` = `checks.Read()` (scorecard check documentation)
- `policy` = constructed from Allstar's `checks` + `threshold` config:
```go
policy := &spol.ScorecardPolicy{
Version: 1,
Policies: map[string]*spol.CheckPolicy{},
}
for _, check := range configuredChecks {
policy.Policies[check] = &spol.CheckPolicy{
Score: int32(threshold),
Mode: spol.CheckPolicy_ENFORCED,
}
}
```
- `opts` = minimal `options.Options{Repo: "owner/repo"}`

## Upload mechanism

**Decision:** Use `go-github/v74`'s `CodeScanning.UploadSarif()`.

scorecard-action delegates upload to the `github/codeql-action/upload-sarif`
GitHub Actions step. Allstar is a standalone app, so it calls the GitHub API
directly.

**GitHub API details:**

- Endpoint: `POST /repos/{owner}/{repo}/code-scanning/sarifs`
- Format: SARIF 2.1.0 only (no other formats supported by GitHub)
- Encoding: gzip compressed, base64 encoded
- Size limit: 10 MB compressed
- Permission: **Code scanning alerts: Read & write** (API scope: `security_events`)
- Response: 202 Accepted

**go-github types:**

```go
type SarifAnalysis struct {
CommitSHA *string `json:"commit_sha,omitempty"`
Ref *string `json:"ref,omitempty"`
Sarif *string `json:"sarif,omitempty"`
ToolName *string `json:"tool_name,omitempty"`
}
```

## Rate limiting

**Decision:** Change detection via commit SHA comparison. Skip the scan and
upload if the repository HEAD hasn't changed since the last upload.

**Rationale:** Allstar scans every 5 minutes. Most repos won't change between
cycles, so the vast majority of uploads get skipped. Comparing the commit SHA
is more reliable than hashing SARIF content, which includes timestamps and
other run-specific metadata that differ between runs even when findings are
identical. The commit SHA approach also avoids the cost of a redundant
scorecard run.

**Implementation:** In-memory map of repo key to last-uploaded commit SHA
(package-level, mutex-protected), following the `pkg/scorecard/scorecard.go`
caching pattern.

**Future improvement:** An `interval` field (`upload: {sarif: true, interval: 24h}`)
could be added if change detection proves insufficient for large orgs.

## Error handling

**Decision:** Non-blocking. Log a warning on upload failure but don't affect
the policy check result (`Result.Pass`).

**Rationale:** The policy check outcome shouldn't depend on whether GitHub
accepted the SARIF upload. Transient API failures (rate limits, timeouts)
should not disrupt the enforcement loop.

## Permission requirements

The Allstar GitHub App requires the **Code scanning alerts** repository
permission (API scope: `security_events`) set to **Read & write** for SARIF
upload. This is a new permission not currently declared.

**Rollout:**
- Self-hosted operators add the permission to their GitHub App immediately
- Public Allstar App (operated by OpenSSF) requires separate coordination
- The feature is dormant until the permission is granted (upload fails with
403, logged as warning)

## Testability

All new functions use package-level mockable function variables, following the
existing pattern in `scorecard.go`:

```go
var (
configFetchConfig func(...) // existing
scorecardGet func(...) // existing
scRun func(...) // existing
sarifUpload func(...) // new
)
```

Tests replace these with mocks. No integration tests against real GitHub API.
125 changes: 125 additions & 0 deletions openspec/changes/evidence-upload/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Proposal: Evidence Upload for Allstar Scorecard Policy

## Summary

Add an evidence upload capability to Allstar's Scorecard policy, starting with
SARIF upload to GitHub's Code Scanning API. This enables organization
administrators to get Scorecard findings in each repository's
**Security > Code Scanning** tab without requiring per-repository workflow setup.

This feature positions Allstar as a downstream evidence consumer aligned with
the [Scorecard v6 direction](https://github.com/ossf/scorecard/pull/4952),
which repositions Scorecard as an "open source security evidence engine."

## Motivation

### Problem

Organizations using Allstar's Scorecard policy get violation notifications via
GitHub issues, but findings do not appear in the GitHub Security tab. This
creates a gap:

- **Org admins** want centralized security visibility across all repositories
- **Security teams** want findings in the standard Security > Code Scanning
dashboard alongside CodeQL, Dependabot, and other SAST/SCA tools
- **Compliance workflows** often require evidence in the Security tab for audit
purposes

The alternative — deploying
[scorecard-action](https://github.com/ossf/scorecard-action) per repository —
requires per-repo workflow setup, which doesn't scale for large organizations.

### Why now

1. **Scorecard v5.4.0** provides `Result.AsSARIF()` for SARIF 2.1.0 generation
2. **Scorecard v6** (ossf/scorecard#4952) defines Allstar as an evidence
consumer — this feature is Phase 1 of that architecture
3. **go-github/v74** (already an Allstar dependency) includes
`CodeScanning.UploadSarif()` for the GitHub Code Scanning API

### Why Allstar

Allstar is the natural home for org-wide SARIF upload because:

- It already monitors all repositories in an organization continuously
- It already runs Scorecard checks via the Scorecard policy
- It has GitHub App authentication with per-installation API access
- It supports org-level configuration with repo-level overrides

scorecard-action generates SARIF and delegates upload to the
`github/codeql-action/upload-sarif` GitHub Actions step. Allstar is a
standalone app (not a GitHub Action), so it uploads SARIF directly via the
GitHub API.

## Current state

Allstar's Scorecard policy (`pkg/policies/scorecard/scorecard.go`):

- Runs configured Scorecard checks individually in a loop
- Compares scores against a configurable threshold
- Creates GitHub issues when checks fail (via the `issue` action)
- Does NOT generate SARIF or upload to Code Scanning

## Scope

### In scope

- **SARIF upload** to GitHub Code Scanning API via `go-github/v74`
- **Configuration** via `upload: {sarif: true}` on `scorecard.yaml`
- **Change detection** to avoid redundant uploads (commit SHA comparison)
- **Non-blocking error handling** (upload failures don't affect policy results)
- **Documentation** for self-hosted operators (permission requirements)

### Out of scope

- Multi-action array refactor (`action: [issue, sarif]`)
- Upload interval configuration (change detection is sufficient for Phase 1)
- v6 evidence formats (in-toto, OSCAL, Gemara) — Phase 2 after v6 ships
- OSPS Baseline conformance enforcement — Phase 3
- Public Allstar App permission update (separate OpenSSF coordination)

### Future phases

| Phase | Deliverable | Trigger |
|-------|-------------|---------|
| Phase 1 (this) | SARIF upload to GitHub Code Scanning | Now |
| Phase 2 | Evidence bundle upload (in-toto, Gemara, OSCAL) | Scorecard v6 ships |
| Phase 3 | OSPS Baseline conformance enforcement at org level | v6 conformance layer stable |

## Ecosystem alignment

Per the [Scorecard v6 proposal](https://github.com/ossf/scorecard/pull/4952):

> Allstar, a Scorecard sub-project, continuously monitors GitHub organizations
> and enforces Scorecard check results as policies. OSPS conformance output
> could enable Allstar to enforce Baseline conformance at the organization level.

Scorecard v6 design principles relevant to this feature:

- **"Evidence is the product."** Scorecard produces evidence; Allstar consumes
and uploads it.
- **"All consumers are equal."** Allstar consumes Scorecard output through
published interfaces (`Result.AsSARIF()`), not special integration.
- **"Formats are presentation."** SARIF is one view of the evidence model.
The upload infrastructure is designed to support additional formats when v6
ships.

## Config file naming

Both Allstar and Scorecard use `scorecard.yaml` but in different locations:

- **Allstar**: `.allstar/scorecard.yaml` (in the `.allstar` org config repo)
- **Scorecard annotations**: `scorecard.yaml` in the repo root or `.github/`

No path overlap. The `upload` key does not conflict with Scorecard's
`annotations` schema.

## Approval

This is a self-contained feature within the Allstar codebase. Per the
PR-driven governance approach:

- Open a well-documented PR on `ossf/allstar`
- Self-hosted operators can use it immediately (requires adding the
**Code scanning alerts** permission to their GitHub App)
- Public Allstar App permission update coordinated separately with OpenSSF
Loading
Loading