Skip to content

Commit 7644555

Browse files
authored
feat: add personas (#226)
1 parent 8c8fecb commit 7644555

23 files changed

+383
-108
lines changed

Diff for: Cargo.lock

+7-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ anyhow = "1.0.93"
1919
clap = { version = "4.5.21", features = ["derive", "env"] }
2020
clap-verbosity-flag = "3.0.0"
2121
env_logger = "0.11.5"
22-
github-actions-models = "0.11.0"
22+
github-actions-models = "0.12.0"
2323
human-panic = "2.0.1"
24+
indexmap = "2.7.0"
2425
indicatif = "0.17.9"
2526
itertools = "0.13.0"
2627
log = "0.4.22"

Diff for: docs/snippets/help.txt

+42-7
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,69 @@ Static analysis for GitHub Actions
33
Usage: zizmor [OPTIONS] <INPUTS>...
44

55
Arguments:
6-
<INPUTS>... The workflow filenames or directories to audit
6+
<INPUTS>...
7+
The workflow filenames or directories to audit
78

89
Options:
910
-p, --pedantic
10-
Emit findings even when the context suggests an explicit security decision made by the user
11+
Emit 'pedantic' findings.
12+
13+
This is an alias for --persona=pedantic.
14+
15+
--persona <PERSONA>
16+
The persona to use while auditing
17+
18+
[default: regular]
19+
20+
Possible values:
21+
- auditor: The "auditor" persona (false positives OK)
22+
- pedantic: The "pedantic" persona (code smells OK)
23+
- regular: The "regular" persona (minimal false positives)
24+
1125
-o, --offline
1226
Only perform audits that don't require network access
27+
1328
-v, --verbose...
1429
Increase logging verbosity
30+
1531
-q, --quiet...
1632
Decrease logging verbosity
33+
1734
-n, --no-progress
1835
Disable the progress bar. This is useful primarily when running with a high verbosity level, as the two will fight for stderr
36+
1937
--gh-token <GH_TOKEN>
20-
The GitHub API token to use [env: GH_TOKEN=]
38+
The GitHub API token to use
39+
40+
[env: GH_TOKEN=]
41+
2142
--format <FORMAT>
22-
The output format to emit. By default, plain text will be emitted [possible values: plain, json, sarif]
43+
The output format to emit. By default, plain text will be emitted
44+
45+
[default: plain]
46+
[possible values: plain, json, sarif]
47+
2348
-c, --config <CONFIG>
2449
The configuration file to load. By default, any config will be discovered relative to $CWD
50+
2551
--no-config
2652
Disable all configuration loading
53+
2754
--no-exit-codes
2855
Disable all error codes besides success and tool failure
56+
2957
--min-severity <MIN_SEVERITY>
30-
Filter all results below this severity [possible values: unknown, informational, low, medium, high]
58+
Filter all results below this severity
59+
60+
[possible values: unknown, informational, low, medium, high]
61+
3162
--min-confidence <MIN_CONFIDENCE>
32-
Filter all results below this confidence [possible values: unknown, low, medium, high]
63+
Filter all results below this confidence
64+
65+
[possible values: unknown, low, medium, high]
66+
3367
-h, --help
34-
Print help
68+
Print help (see a summary with '-h')
69+
3570
-V, --version
3671
Print version

Diff for: docs/usage.md

+88
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,94 @@ See [Integration](#integration) for suggestions on when to use each format.
6969

7070
All other exit codes are currently reserved.
7171

72+
## Using personas
73+
74+
!!! tip
75+
76+
`--persona=...` is available in `v0.7.0` and later.
77+
78+
`zizmor` comes with three pre-defined "personas," which dictate how
79+
sensitive `zizmor`'s analyses are:
80+
81+
* The _regular persona_: the user wants high-signal, low-noise, actionable
82+
security findings. This persona is best for ordinary local use as well as use
83+
in most CI/CD setups, which is why it's the default.
84+
85+
!!! note
86+
87+
This persona can be made explicit with `--persona=regular`,
88+
although this is not required.
89+
90+
91+
* The _pedantic persona_, enabled by `--persona=pedantic`: the user wants
92+
*code smells* in addition to regular, actionable security findings.
93+
94+
This persona is ideal for finding things that are a good idea
95+
to clean up or resolve, but are likely not immediately actionable
96+
security findings (or are actionable, but indicate a intentional
97+
security decision by the workflow author).
98+
99+
For example, using the pedantic persona will flag the following
100+
with an `unpinned-uses` finding, since it uses a symbolic reference
101+
as its pin instead of a hashed pin:
102+
103+
```yaml
104+
uses: actions/checkout@v3
105+
```
106+
107+
produces:
108+
109+
```console
110+
$ zizmor --pedantic tests/test-data/unpinned-uses.yml
111+
help[unpinned-uses]: unpinned action reference
112+
--> tests/test-data/unpinned-uses.yml:14:9
113+
|
114+
14 | - uses: actions/checkout@v3
115+
| ------------------------- help: action is not pinned to a hash ref
116+
|
117+
= note: audit confidence → High
118+
```
119+
120+
!!! tip
121+
122+
This persona can also be enabled with `--pedantic`, which is
123+
an alias for `--persona=pedantic`.
124+
125+
* The _auditor persona_, enabled by `--persona=auditor`: the user wants
126+
*everything* flagged by `zizmor`, including findings that are likely
127+
to be false positives.
128+
129+
This persona is ideal for security auditors and code reviewers, who
130+
want to go through `zizmor`'s findings manually with a fine-toothed comb.
131+
132+
Some audits, notably `self-hosted-runner`, *only* produce auditor-level
133+
results. This is because these audits require runtime context that `zizmor`
134+
lacks access to by design, meaning that their results are always
135+
subject to false positives.
136+
137+
For example, with the default persona:
138+
139+
```console
140+
$ zizmor tests/test-data/self-hosted.yml
141+
🌈 completed self-hosted.yml
142+
No findings to report. Good job! (1 suppressed)
143+
```
144+
145+
and with `--persona=auditor`:
146+
147+
```console
148+
$ zizmor --persona=auditor tests/test-data/self-hosted.yml
149+
note[self-hosted-runner]: runs on a self-hosted runner
150+
--> tests/test-data/self-hosted.yml:8:5
151+
|
152+
8 | runs-on: [self-hosted, my-ubuntu-box]
153+
| ------------------------------------- note: self-hosted runner used here
154+
|
155+
= note: audit confidence → High
156+
157+
1 finding: 1 unknown, 0 informational, 0 low, 0 medium, 0 high
158+
```
159+
72160
## Filtering results
73161

74162
There are two straightforward ways to filter `zizmor`'s results:

Diff for: src/audit/artipacked.rs

+10-14
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ use itertools::Itertools;
99

1010
use super::{audit_meta, WorkflowAudit};
1111
use crate::{
12-
finding::{Confidence, Finding, Severity},
12+
finding::{Confidence, Finding, Persona, Severity},
1313
state::AuditState,
1414
};
1515
use crate::{models::Workflow, utils::split_patterns};
1616

17-
pub(crate) struct Artipacked {
18-
pub(crate) state: AuditState,
19-
}
17+
pub(crate) struct Artipacked;
2018

2119
audit_meta!(
2220
Artipacked,
@@ -47,8 +45,8 @@ impl Artipacked {
4745
}
4846

4947
impl WorkflowAudit for Artipacked {
50-
fn new(state: AuditState) -> Result<Self> {
51-
Ok(Self { state })
48+
fn new(_state: AuditState) -> Result<Self> {
49+
Ok(Self)
5250
}
5351

5452
fn audit<'w>(&self, workflow: &'w Workflow) -> Result<Vec<Finding<'w>>> {
@@ -76,15 +74,11 @@ impl WorkflowAudit for Artipacked {
7674
Some(EnvValue::Boolean(true)) => {
7775
// If a user explicitly sets `persist-credentials: true`,
7876
// they probably mean it. Only report if being pedantic.
79-
if self.state.pedantic {
80-
vulnerable_checkouts.push(step)
81-
} else {
82-
continue;
83-
}
77+
vulnerable_checkouts.push((step, Persona::Pedantic))
8478
}
8579
// TODO: handle expressions and literal strings here.
8680
// persist-credentials is true by default.
87-
_ => vulnerable_checkouts.push(step),
81+
_ => vulnerable_checkouts.push((step, Persona::default())),
8882
}
8983
} else if uses.starts_with("actions/upload-artifact") {
9084
let Some(EnvValue::String(path)) = with.get("path") else {
@@ -102,11 +96,12 @@ impl WorkflowAudit for Artipacked {
10296
if vulnerable_uploads.is_empty() {
10397
// If we have no vulnerable uploads, then emit lower-confidence
10498
// findings for just the checkout steps.
105-
for checkout in vulnerable_checkouts {
99+
for (checkout, persona) in vulnerable_checkouts {
106100
findings.push(
107101
Self::finding()
108102
.severity(Severity::Medium)
109103
.confidence(Confidence::Low)
104+
.persona(persona)
110105
.add_location(
111106
checkout
112107
.location()
@@ -119,7 +114,7 @@ impl WorkflowAudit for Artipacked {
119114
// Select only pairs where the vulnerable checkout precedes the
120115
// vulnerable upload. There are more efficient ways to do this than
121116
// a cartesian product, but this way is simple.
122-
for (checkout, upload) in vulnerable_checkouts
117+
for ((checkout, persona), upload) in vulnerable_checkouts
123118
.into_iter()
124119
.cartesian_product(vulnerable_uploads.into_iter())
125120
{
@@ -128,6 +123,7 @@ impl WorkflowAudit for Artipacked {
128123
Self::finding()
129124
.severity(Severity::High)
130125
.confidence(Confidence::High)
126+
.persona(persona)
131127
.add_location(
132128
checkout
133129
.location()

Diff for: src/audit/github_env.rs

-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ mod tests {
172172
("something | tee \"${$OTHER_ENV}\" # not $GITHUB_ENV", false),
173173
] {
174174
let audit_state = AuditState {
175-
pedantic: false,
176175
offline: false,
177176
gh_token: None,
178177
caches: Caches::new(),

Diff for: src/audit/self_hosted_runner.rs

+9-12
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
//! which are frequently unsafe to use in public repositories
33
//! due to the potential for persistence between workflow runs.
44
//!
5-
//! This audit is "pedantic" only, since zizmor can't detect
5+
//! This audit is "auditor" only, since zizmor can't detect
66
//! whether self-hosted runners are ephemeral or not.
77
88
use crate::{
9-
finding::{Confidence, Severity},
9+
finding::{Confidence, Persona, Severity},
1010
AuditState,
1111
};
1212

@@ -18,9 +18,7 @@ use github_actions_models::{
1818

1919
use super::{audit_meta, WorkflowAudit};
2020

21-
pub(crate) struct SelfHostedRunner {
22-
pub(crate) state: AuditState,
23-
}
21+
pub(crate) struct SelfHostedRunner;
2422

2523
audit_meta!(
2624
SelfHostedRunner,
@@ -29,11 +27,11 @@ audit_meta!(
2927
);
3028

3129
impl WorkflowAudit for SelfHostedRunner {
32-
fn new(state: AuditState) -> anyhow::Result<Self>
30+
fn new(_state: AuditState) -> anyhow::Result<Self>
3331
where
3432
Self: Sized,
3533
{
36-
Ok(Self { state })
34+
Ok(Self)
3735
}
3836

3937
fn audit<'w>(
@@ -42,11 +40,6 @@ impl WorkflowAudit for SelfHostedRunner {
4240
) -> Result<Vec<crate::finding::Finding<'w>>> {
4341
let mut results = vec![];
4442

45-
if !self.state.pedantic {
46-
log::info!("skipping self-hosted runner checks");
47-
return Ok(results);
48-
}
49-
5043
for job in workflow.jobs() {
5144
let Job::NormalJob(normal) = *job else {
5245
continue;
@@ -66,6 +59,7 @@ impl WorkflowAudit for SelfHostedRunner {
6659
Self::finding()
6760
.confidence(Confidence::High)
6861
.severity(Severity::Unknown)
62+
.persona(Persona::Auditor)
6963
.add_location(
7064
job.location()
7165
.with_keys(&["runs-on".into()])
@@ -82,6 +76,7 @@ impl WorkflowAudit for SelfHostedRunner {
8276
Self::finding()
8377
.confidence(Confidence::Low)
8478
.severity(Severity::Unknown)
79+
.persona(Persona::Auditor)
8580
.add_location(
8681
job.location().with_keys(&["runs-on".into()]).annotated(
8782
"expression may expand into a self-hosted runner",
@@ -101,6 +96,7 @@ impl WorkflowAudit for SelfHostedRunner {
10196
Self::finding()
10297
.confidence(Confidence::Low)
10398
.severity(Severity::Unknown)
99+
.persona(Persona::Auditor)
104100
.add_location(
105101
job.location()
106102
.with_keys(&["runs-on".into()])
@@ -114,6 +110,7 @@ impl WorkflowAudit for SelfHostedRunner {
114110
Self::finding()
115111
.confidence(Confidence::Low)
116112
.severity(Severity::Unknown)
113+
.persona(Persona::Auditor)
117114
.add_location(
118115
job.location()
119116
.with_keys(&["runs-on".into()])

0 commit comments

Comments
 (0)