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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ jobs:
- name: Run tests
run: cargo test --workspace

# Overrides must match GitHub's canonical repository names. Catch typos,
# renamed/deleted repositories, and incorrect casing before an override is
# silently skipped.
# Limitation: `github.token` cannot read private repositories, so those fail here with a 404.
- name: Validate policy repositories
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
for repository in $(yq '.repositories | keys | .[]' zizmor-policy.yml); do
canonical=$(gh api "repos/$repository" --jq .full_name)
if [[ "$repository" != "$canonical" ]]; then
echo "Policy repository '$repository' must use GitHub's canonical name '$canonical'." >&2
exit 1
fi
done

typos:
name: Typos
runs-on: ubuntu-24.04
Expand Down
18 changes: 15 additions & 3 deletions .github/workflows/crabwatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,31 @@ jobs:
persist-credentials: false

- name: Download crabwatch zizmor config
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api repos/rust-lang/crabwatch/contents/zizmor-default.yml \
-H "Accept: application/vnd.github.raw+json" > zizmor-default.yml
gh api repos/rust-lang/crabwatch/contents/zizmor-policy.yml \
-H "Accept: application/vnd.github.raw+json" > "$RUNNER_TEMP/zizmor-policy.yml"

# Apply this repository's overrides to the default configuration.
# zizmor-action runs zizmor in a container that mounts only the
# workspace, so the config must be written inside it.
yq --exit-status \
'.default * (.repositories[strenv(GITHUB_REPOSITORY)] // {})' \
"$RUNNER_TEMP/zizmor-policy.yml" > crabwatch-zizmor.yml

echo "::group::Effective zizmor configuration for $GITHUB_REPOSITORY"
cat crabwatch-zizmor.yml
echo "::endgroup::"
Comment on lines +44 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since clicking the status check

Image

leads to a job summary, perhaps we could write this (also?) to the job summary, so developers see the config without having to expanding the logs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If useful we could do this in another PR πŸ‘


- name: Run zizmor
# A missing or empty root .github directory has nothing to audit.
if: ${{ hashFiles('.github/**') != '' }}
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
with:
advanced-security: false
config: zizmor-default.yml
config: crabwatch-zizmor.yml
# Only lint the root .github directory.
# Ignore nested .github directories because they can belong to
# vendored projects or test fixtures.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ serde_json = "1"
futures = "0.3.33"
log = "0.4.33"
env_logger = "0.11.11"
tempfile = "3.27.0"

[dev-dependencies]
insta = "1.48.0"
tempfile = "3.27.0"
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,38 @@ It provides:
## Checks

Crabwatch runs [zizmor](https://docs.zizmor.sh/) with the
[`zizmor-default.yml`](./zizmor-default.yml) configuration file, maintained by
the Rust Infrastructure team.
[`zizmor-policy.yml`](./zizmor-policy.yml) policy, maintained by the Rust
Infrastructure team. Its `default` section contains the shared zizmor
configuration, and `repositories` contains repository-specific overrides.

### Repository overrides

Add an `owner/repository` entry under `repositories` to change a rule's settings
for that repository, for example to disable a rule or to opt in to a rule that is
disabled by default.
Overrides are deep-merged into the default, so unspecified settings are
inherited, but a list such as `ignore` replaces the default's list rather than
extending it.
CI validates the policy's structure and that every repository key is GitHub's
canonical `owner/repository` name, since the lookup is case-sensitive.

Example (hypothetical):

```yaml
repositories:
rust-lang/rust-clippy:
rules:
bot-conditions:
# Accepted exception for this repository's automation pattern.
disable: true
rust-lang/rust:
rules:
bot-conditions:
disable: true
overprovisioned-secrets:
# Temporary exception while its workflows are migrated.
disable: true
```

## Design principles

Expand All @@ -42,6 +72,9 @@ manually auditing repositories with the same configuration.

## CLI usage

The CLI determines the repository's configuration in the same way as the workflow,
so you need to install [mikefarah's `yq` v4](https://github.com/mikefarah/yq#install).

Analyze every eligible repository in a GitHub organization:

```console
Expand Down
29 changes: 13 additions & 16 deletions src/command/analyze.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{clone, github, scan};
use crate::{clone, config, github, scan};
use anyhow::{Context as _, anyhow, bail};
use futures::stream::StreamExt;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -104,10 +104,14 @@ async fn analyze_repo(
client: &reqwest::Client,
parsed: &ParsedRepo,
crabwatch_dir: &Path,
zizmor_config: &Path,
policy: &config::ZizmorPolicy,
github_token: &str,
) -> anyhow::Result<(String, scan::ScanOutcome)> {
let sha = github::fetch_head_commit(client, &parsed.org, &parsed.repo, github_token).await?;
let head = github::fetch_head_commit(client, &parsed.org, &parsed.repo, github_token).await?;
// GitHub's canonical name selects the policy overrides. Kept alive until
// the scan finishes; the file is deleted on drop.
let config_file = policy.write_config(&head.name_with_owner).await?;
let sha = head.sha;
log::debug!("HEAD commit: {sha}");
let path = cache_path(parsed, crabwatch_dir, &sha);
let repo_cache_dir = path
Expand All @@ -127,7 +131,7 @@ async fn analyze_repo(
clone::clone_repo(&parsed.org, &parsed.repo, github_token, &path, &sha).await?;
}

let report = scan::scan_workflows(&path, zizmor_config, github_token).await?;
let report = scan::scan_workflows(&path, config_file.path(), github_token).await?;

Ok((report.output.trim_end().to_string(), report.outcome))
}
Expand All @@ -140,18 +144,12 @@ pub async fn run(
) -> anyhow::Result<()> {
let client = reqwest::Client::new();
let crabwatch_dir = crabwatch_dir(cache_dir_override)?;
let zizmor_config = scan::sync_zizmor_config(&crabwatch_dir)?;
let policy = config::ZizmorPolicy::bundled().await?;

if let Some(repo_arg) = repo_arg {
let parsed = parse_repo(&repo_arg)?;
let (output, outcome) = analyze_repo(
&client,
&parsed,
&crabwatch_dir,
&zizmor_config,
github_token,
)
.await?;
let (output, outcome) =
analyze_repo(&client, &parsed, &crabwatch_dir, &policy, github_token).await?;
match outcome {
scan::ScanOutcome::Findings => log::info!("{output}"),
scan::ScanOutcome::Clean => log::info!("No findings to report."),
Expand All @@ -164,7 +162,7 @@ pub async fn run(

let client = &client;
let crabwatch_dir = &crabwatch_dir;
let zizmor_config = &zizmor_config;
let policy = &policy;
let org = &org;
let mut failures = Vec::new();
let mut any_findings = false;
Expand All @@ -177,8 +175,7 @@ pub async fn run(
};
async move {
let result =
analyze_repo(client, &parsed, crabwatch_dir, zizmor_config, github_token)
.await;
analyze_repo(client, &parsed, crabwatch_dir, policy, github_token).await;
(parsed, result)
}
})
Expand Down
Loading
Loading