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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
mod audit;
mod audit_patterns;
mod audited_actions;
mod auth;
mod config;
mod github;
mod output;
mod pin;
mod score;
mod update;
mod workflow;
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell;
use colored::control;
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Clone, Copy, PartialEq, clap::ValueEnum)]
enum ColorMode {
Always,
Auto,
Never,
}
#[derive(Parser)]
#[command(
name = "pinprick",
about = "GitHub Actions supply chain security",
version,
propagate_version = true
)]
struct Cli {
/// When to use colors: auto, always, never
#[arg(long, default_value = "auto", global = true)]
color: ColorMode,
/// Output as JSON
#[arg(long, global = true)]
json: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Audit actions for runtime fetch risks
Audit {
/// Repository root
#[arg(default_value = ".")]
path: PathBuf,
/// Show every matched outbound-call pattern, including ones that
/// passed the version check (useful for CI audit logs)
#[arg(short, long)]
verbose: bool,
/// Output findings as SARIF 2.1.0 (for github/codeql-action/upload-sarif)
#[arg(long, conflicts_with = "json")]
sarif: bool,
},
/// Remove locally cached audit results
Clean,
/// Generate shell completions
Completions {
/// Shell to generate completions for
shell: Shell,
},
/// Pin action references to full SHAs
Pin {
/// Repository root
#[arg(default_value = ".")]
path: PathBuf,
/// Write changes to files (default is dry-run)
#[arg(long = "write")]
apply: bool,
},
/// Score a repository's Actions supply chain posture
Score {
/// Repository root
#[arg(default_value = ".")]
path: PathBuf,
/// Emit a self-contained HTML report to stdout
///
/// If `--json` is also set, JSON wins (global flags take precedence).
#[arg(long)]
html: bool,
},
/// Check for updates to pinned actions
Update {
/// Repository root
#[arg(default_value = ".")]
path: PathBuf,
/// Write changes to files (default is dry-run)
#[arg(long = "write")]
apply: bool,
/// Only check actions whose owner/repo contains this substring
/// (e.g., `actions/checkout`, `actions/` for the whole org)
#[arg(long, value_name = "PATTERN")]
only: Option<String>,
},
}
#[tokio::main]
async fn main() -> ExitCode {
let cli = Cli::parse();
match cli.color {
ColorMode::Always => control::set_override(true),
ColorMode::Never => control::set_override(false),
ColorMode::Auto => {}
}
let result = match &cli.command {
Command::Audit {
path,
verbose,
sarif,
} => {
let config = config::Config::load(path);
audit::run(path, cli.json, *sarif, *verbose, &config).await
}
Command::Clean => {
let removed = match audited_actions::cache_dir() {
Some(dir) if dir.is_dir() => std::fs::remove_dir_all(&dir).is_ok(),
_ => false,
};
if cli.json {
let msg = serde_json::json!({ "cleaned": removed });
println!("{msg}");
} else if removed {
println!("Cache cleaned.");
} else {
println!("Nothing to clean.");
}
return ExitCode::SUCCESS;
}
Command::Completions { shell } => {
clap_complete::generate(
*shell,
&mut Cli::command(),
"pinprick",
&mut std::io::stdout(),
);
return ExitCode::SUCCESS;
}
Command::Pin { path, apply } => pin::run(path, cli.json, *apply).await,
Command::Score { path, html } => score::run(path, cli.json, *html).await,
Command::Update { path, apply, only } => {
update::run(path, *apply, cli.json, only.as_deref()).await
}
};
match result {
Ok(code) => code,
Err(e) => {
if cli.json {
let err = serde_json::json!({ "error": format!("{e:#}") });
eprintln!("{err}");
} else {
eprintln!("error: {e:#}");
}
ExitCode::from(2)
}
}
}