1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# cargo audit: the RustSec advisory database against the checked-in lockfile,
# on a clock.
#
# ci.yml already runs cargo-deny, which checks the same advisory database over
# the same graph. This is not that check run twice; it is the same check run
# at the time the other one cannot run.
#
# An advisory is published against a crate that has been in Cargo.lock for
# eight months. No commit is coming -- the dependency did not change, the code
# did not change, nothing triggers a pull request. ci.yml's `deny` job only
# fires on push and pull_request, so it will next look at that lockfile
# whenever somebody happens to open a pull request, which could be tomorrow or
# could be in three weeks. The advisory sits undetected for the gap.
#
# That gap is the entire reason this file exists. The daily cron closes it to
# at most 24 hours. The `Cargo.lock` path trigger is the cheap half: a
# dependency bump gets an advisory answer in seconds rather than waiting for
# the full cargo-deny graph resolve in ci.yml.
#
# Division of labour, so neither grows into the other:
#
# deny.toml / ci.yml licences, banned crates, registries, yanked crates,
# duplicate versions. The policy lives there. Advisory
# ignores go in deny.toml's `[advisories] ignore`.
# this file advisories, on a schedule, and nothing else.
name: Audit
on:
push:
branches:
paths:
- "Cargo.lock"
- "Cargo.toml"
- "macros/Cargo.toml"
- ".github/workflows/audit.yml"
pull_request:
paths:
- "Cargo.lock"
- "Cargo.toml"
- "macros/Cargo.toml"
- ".github/workflows/audit.yml"
schedule:
# Daily. Minute is not :00, and 03:38 sits clear of ci.yml's 04:17
# powerset so the two are not competing for runners.
- cron: "38 3 * * *"
workflow_dispatch:
# Reading the lockfile is the whole job. A finding surfaces as a failed run,
# which GitHub already mails to watchers -- `rustsec/audit-check` would open
# an issue instead and wants `issues: write` for it, which is a repository
# write granted to a scheduled job for a notification the platform sends for
# free.
permissions:
contents: read
concurrency:
group: audit-${{ github.ref }}
# Never on the scheduled run. A cancelled nightly is a day with no advisory
# answer, and this workflow exists precisely to guarantee there is one every
# day. On a branch under review a new push supersedes the old lockfile, so
# cancelling there is free.
cancel-in-progress: ${{ github.event_name != 'schedule' }}
jobs:
audit:
name: RustSec advisories
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# `cargo audit` is a cargo subcommand, so it needs a toolchain on PATH
# even though it compiles nothing -- it parses Cargo.lock.
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: stable
# Same installer ci.yml uses for cargo-hack and mdbook. A prebuilt
# binary, not `cargo install`, which would be a two-minute compile on
# every one of these runs.
- uses: taiki-e/install-action@a2a5f6e99e1a31540baa0468acfa302cff0f359f # v2.86.4
with:
tool: cargo-audit
# Plain `cargo audit`: fails on vulnerabilities, warns on unmaintained
# and unsound. Not `--deny warnings`. An `unmaintained` advisory often
# has no fixed version to move to -- the crate is unmaintained, that is
# the finding -- and a daily job that fails red with no available action
# gets muted within a week, taking the vulnerability signal with it.
# If a specific unmaintained crate should block, that decision belongs
# in deny.toml with a reason attached, not as a blanket flag here.
#
# RUSTSEC-2026-0235 (rkyv 0.7, out-of-bounds read on a malformed
# archive) is ignored here and nowhere else. `rust_decimal` declares
# `rkyv` as an optional dependency and nothing in this workspace turns
# that feature on, so the crate is never compiled: `cargo tree -i rkyv
# -e all --target all` finds no path to it, and ci.yml's cargo-deny job
# -- which resolves features and therefore never sees it -- passes.
# Cargo.lock records the maximal resolve rather than the built graph,
# which is why `cargo audit` reports it and cargo-deny does not.
#
# This is the one case the division of labour above does not cover: the
# ignore cannot live in deny.toml, because cargo-deny has nothing to
# ignore. Drop the flag when `rust_decimal` releases a version whose
# optional `rkyv` dependency is >=0.8.17, or when the workspace stops
# depending on `rust_decimal` at all.
- name: cargo audit
run: cargo audit --ignore RUSTSEC-2026-0235