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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! Audits workflows for usage of self-hosted runners,
//! which are frequently unsafe to use in public repositories
//! due to the potential for persistence between workflow runs.
//!
//! This audit is "auditor" only, since zizmor can't detect
//! whether self-hosted runners are ephemeral or not.
use github_actions_models::{
common::expr::{ExplicitExpr, LoE},
workflow::job::RunsOn,
};
use super::{Audit, AuditLoadError, Job, audit_meta};
use crate::finding::location::Locatable as _;
use crate::{
AuditState,
audit::AuditError,
finding::{Confidence, Persona, Severity},
};
pub(crate) struct SelfHostedRunner;
audit_meta!(
SelfHostedRunner,
"self-hosted-runner",
"runs on a self-hosted runner"
);
#[async_trait::async_trait]
impl Audit for SelfHostedRunner {
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
where
Self: Sized,
{
Ok(Self)
}
async fn audit_workflow<'doc>(
&self,
workflow: &'doc crate::models::workflow::Workflow,
_config: &crate::config::Config,
) -> Result<Vec<crate::finding::Finding<'doc>>, AuditError> {
let mut results = vec![];
for job in workflow.jobs() {
let Job::NormalJob(job) = job else {
continue;
};
match &job.runs_on {
LoE::Literal(RunsOn::Target(labels)) => {
{
let Some(label) = labels.first() else {
continue;
};
if label == "self-hosted" {
// All self-hosted runners start with the 'self-hosted'
// label followed by any specifiers.
results.push(
Self::finding()
.confidence(Confidence::High)
.severity(Severity::Medium)
.persona(Persona::Auditor)
.add_location(
job.location()
.primary()
.with_keys(["runs-on".into()])
.annotated("self-hosted runner used here"),
)
.build(workflow)?,
);
} else if ExplicitExpr::from_curly(label).is_some() {
// The job might also have its runner expanded via an
// expression. Long-term we should perform this evaluation
// to increase our confidence, but for now we flag it as
// potentially expanding to self-hosted.
results.push(
Self::finding()
.confidence(Confidence::Low)
.severity(Severity::Medium)
.persona(Persona::Auditor)
.add_location(
job.location()
.primary()
.with_keys(["runs-on".into()])
.annotated(
"expression may expand into a self-hosted runner",
),
)
.build(workflow)?,
);
}
}
}
// NOTE: GHA docs are unclear on whether runner groups always
// imply self-hosted runners or not. All examples suggest that they
// do, but I'm not sure.
// See: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/managing-access-to-self-hosted-runners-using-groups
// See: https://docs.github.com/en/actions/writing-workflows/choosing-where-your-workflow-runs/choosing-the-runner-for-a-job
LoE::Literal(RunsOn::Group { .. }) => results.push(
Self::finding()
.confidence(Confidence::Low)
.severity(Severity::Medium)
.persona(Persona::Auditor)
.add_location(
job.location()
.primary()
.with_keys(["runs-on".into()])
.annotated("runner group implies self-hosted runner"),
)
.build(workflow)?,
),
// The entire `runs-on:` is an expression, which may or may
// not be a self-hosted runner when expanded, like above.
LoE::Expr(exp) => {
let Some(matrix) = job.matrix() else {
continue;
};
let self_hosted = matrix.expansions().iter().any(|expansion| {
exp.as_bare() == expansion.path && expansion.value.contains("self-hosted")
});
if self_hosted {
results.push(
Self::finding()
.confidence(Confidence::High)
.severity(Severity::Medium)
.persona(Persona::Auditor)
.add_location(
job.location()
.with_keys(["strategy".into()])
.annotated("matrix declares self-hosted runner"),
)
.add_location(
job.location()
.primary()
.with_keys(["runs-on".into()])
.annotated(
"expression may expand into a self-hosted runner",
),
)
.build(workflow)?,
)
}
}
}
}
Ok(results)
}
}