1#![forbid(unsafe_code)]
7
8mod bulk;
11mod cli;
12mod clients;
13mod commands;
14mod debug;
15mod dlp;
16mod env;
17mod error;
18mod files;
19mod input;
20mod json;
21mod modelsec;
22mod output;
23mod pagination;
24mod policy;
25mod redteam;
26mod render;
27mod report;
28mod runtime;
29mod segment;
30mod signals;
31mod store;
32mod tenant;
33#[cfg(test)]
34mod test_support;
35mod views;
36mod walk;
37mod writes;
38
39use clap::FromArgMatches;
40use commands::{Invocation, Plan};
41use error::{CliError, EXIT_OK, EXIT_OPERATIONAL, EXIT_USAGE};
42use output::Output;
43use std::ffi::OsString;
44use std::io::{ErrorKind, Write};
45
46pub const VERSION: &str = env!("CARGO_PKG_VERSION");
48
49pub fn run<I, T>(args: I) -> i32
51where
52 I: IntoIterator<Item = T>,
53 T: Into<OsString>,
54{
55 let args: Vec<OsString> = args.into_iter().map(Into::into).collect();
56 if version_requested(&args) {
57 return print_version();
58 }
59 let parsed = cli::command()
60 .try_get_matches_from(&args)
61 .and_then(|matches| cli::Cli::from_arg_matches(&matches));
62 let cli = match parsed {
63 Ok(cli) => cli,
64 Err(error) => {
65 let error = cli::redacted_parse_error(&args, &error).unwrap_or(error);
68 let _ = error.print();
69 return error.exit_code();
70 }
71 };
72 let mut output = Output::stdio(cli.global.quiet);
73 output.set_debug(cli.global.debug);
74 if commands::uses_format_alias(&cli) {
76 output.warning(commands::FORMAT_DEPRECATED);
77 }
78 if let Some(cli::TopCommand::Redteam(group)) = &cli.command {
80 for warning in commands::redteam::deprecation_warnings(group) {
81 output.warning(warning);
82 }
83 }
84 if let Some(group) = commands::dlp_group(&cli) {
86 for warning in commands::dlp::deprecation_warnings(group) {
87 output.warning(warning);
88 }
89 }
90 if commands::uses_input_alias(&cli) {
92 output.warning(commands::INPUT_DEPRECATED);
93 }
94 let plan = match commands::plan(&cli) {
95 Ok(plan) => plan,
96 Err(error) => {
97 output.report(&error);
98 return error.exit_code(None);
99 }
100 };
101 if let Plan::GroupHelp(path) = plan {
102 output.write_stderr(&cli::group_help(path));
103 return EXIT_USAGE;
104 }
105 let mut invocation = Invocation::new(output, cli.global.tenant.clone());
106 if cli.global.debug
108 && plan.debug_policy() == debug::DebugPolicy::Log
109 && let Err(error) = invocation.open_debug_log()
110 {
111 invocation.output.report(&error);
112 return error.exit_code(None);
113 }
114 execute(plan, invocation)
115}
116
117pub fn version_requested(args: &[OsString]) -> bool {
120 args.iter()
121 .skip(1)
122 .take_while(|arg| *arg != "--")
123 .any(|arg| arg == "-V" || arg == "--version")
124}
125
126fn stdout_write_failed() -> CliError {
129 CliError::operational("AISEC_SDK_ERROR:Output could not be written to stdout")
130}
131
132fn print_version() -> i32 {
133 let mut stdout = std::io::stdout().lock();
134 match writeln!(stdout, "{VERSION}").and_then(|()| stdout.flush()) {
135 Err(error) if error.kind() != ErrorKind::BrokenPipe => {
136 let text = stdout_write_failed().render_colored(render::style::stderr_color());
137 let _ = std::io::stderr().write_all(text.as_bytes());
138 EXIT_OPERATIONAL
139 }
140 _ => EXIT_OK,
141 }
142}
143
144fn execute(plan: Plan, mut invocation: Invocation) -> i32 {
145 let runtime = match tokio::runtime::Builder::new_current_thread()
146 .enable_all()
147 .build()
148 {
149 Ok(runtime) => runtime,
150 Err(_) => {
151 let error = CliError::operational("AISEC_SDK_ERROR:The async runtime could not start");
152 invocation.output.report(&error);
153 return EXIT_OPERATIONAL;
154 }
155 };
156 let signals = signals::install(invocation.session.clone());
157 signals.set_quiet(invocation.output.quiet());
158 invocation.output.attach_signals(signals.clone());
159 let fallback = plan.interrupt_context();
160 let result = runtime.block_on(commands::dispatch(&mut invocation, plan));
161 let fallback = if invocation.committed {
163 error::InterruptContext::ChangeSaved
164 } else {
165 invocation.interrupt.take().unwrap_or(fallback)
166 };
167 runtime.shutdown_background();
170 let signal = signals.code();
171 let result = match (result, signal) {
173 (Err(CliError::Interrupted(context)), _) => Err(CliError::Interrupted(context)),
174 (_, Some(_)) => Err(CliError::Interrupted(fallback)),
175 (result, None) => result,
176 };
177 finish(&mut invocation.output, result, signal)
178}
179
180fn finish(output: &mut Output, result: Result<(), CliError>, signal: Option<i32>) -> i32 {
181 match result {
182 Ok(()) => match output.commit() {
183 Ok(()) => EXIT_OK,
184 Err(error) if error.kind() == ErrorKind::BrokenPipe => EXIT_OK,
186 Err(_) => {
187 let error = match output.pending_signal() {
189 Some(_) => CliError::Interrupted(error::InterruptContext::OutputIncomplete),
190 None => stdout_write_failed(),
191 };
192 output.report(&error);
193 error.exit_code(output.pending_signal())
194 }
195 },
196 Err(error) => {
197 output.discard();
198 output.report(&error);
199 error.exit_code(signal)
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 fn args(list: &[&str]) -> Vec<OsString> {
209 list.iter().map(OsString::from).collect()
210 }
211
212 #[test]
213 fn version_pre_scan() {
214 assert!(version_requested(&args(&["airs-cli-rs", "-V"])));
215 assert!(version_requested(&args(&[
216 "airs-cli-rs",
217 "runtime",
218 "scan",
219 "--profile",
220 "-V",
221 "hi"
222 ])));
223 assert!(version_requested(&args(&[
224 "airs-cli-rs",
225 "doctor",
226 "--version"
227 ])));
228 assert!(!version_requested(&args(&[
229 "airs-cli-rs",
230 "runtime",
231 "scan",
232 "--profile",
233 "p",
234 "--",
235 "-V"
236 ])));
237 assert!(
238 !version_requested(&args(&["-V"])),
239 "the program name is skipped"
240 );
241 assert!(!version_requested(&args(&["airs-cli-rs", "-v"])));
242 }
243
244 #[test]
245 fn finish_maps_results_to_exit_codes() {
246 let (mut out, stdout, stderr) = output::testing::capture(false);
247 out.data_line("data");
248 assert_eq!(finish(&mut out, Ok(()), None), 0);
249 assert_eq!(stdout.text(), "data\n");
250
251 let (mut out, stdout, stderr2) = output::testing::capture(false);
252 out.data_line("partial");
253 assert_eq!(finish(&mut out, Err(CliError::usage("bad")), None), 2);
254 assert_eq!(stdout.text(), "");
255 assert_eq!(stderr2.text(), "\n ✗ bad\n\n");
256
257 let (mut out, _, stderr3) = output::testing::capture(false);
258 let code = finish(
259 &mut out,
260 Err(CliError::Interrupted(
261 error::InterruptContext::NoDataWritten,
262 )),
263 Some(143),
264 );
265 assert_eq!(code, 143);
266 assert!(stderr3.text().contains("Interrupted; no data was written"));
267 assert_eq!(stderr.text(), "");
268 }
269
270 #[test]
271 fn broken_pipe_is_success() {
272 struct Closed;
273 impl Write for Closed {
274 fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
275 Err(std::io::Error::from(ErrorKind::BrokenPipe))
276 }
277 fn flush(&mut self) -> std::io::Result<()> {
278 Ok(())
279 }
280 }
281 let mut out = Output::with_sinks(false, Box::new(Closed), Box::new(std::io::sink()));
282 out.data_line("data");
283 assert_eq!(finish(&mut out, Ok(()), None), 0);
284 }
285
286 #[test]
287 fn other_stdout_write_failures_are_reported() {
288 struct Full;
289 impl Write for Full {
290 fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
291 Err(std::io::Error::from(ErrorKind::StorageFull))
292 }
293 fn flush(&mut self) -> std::io::Result<()> {
294 Ok(())
295 }
296 }
297 let stderr = output::testing::Capture::default();
298 let mut out = Output::with_sinks(false, Box::new(Full), Box::new(stderr.clone()));
299 out.data_line("data");
300 assert_eq!(finish(&mut out, Ok(()), None), 1);
301 assert_eq!(
302 stderr.text(),
303 "\n ✗ Error: AISEC_SDK_ERROR:Output could not be written to stdout\n\n"
304 );
305 }
306
307 #[test]
308 fn a_signal_before_the_final_write_reports_the_interruption() {
309 let (mut out, stdout, stderr) = output::testing::capture(false);
310 let signals = std::sync::Arc::new(signals::SignalState::default());
311 assert_eq!(signals.on_signal(143), signals::Action::Cancel);
312 out.attach_signals(signals);
313 out.data_line("data");
314 assert_eq!(finish(&mut out, Ok(()), None), 143);
315 assert_eq!(stdout.text(), "", "nothing is written after a signal");
316 assert_eq!(
317 stderr.text(),
318 "\n ✗ Error: Interrupted; the output was not completely written\n\n"
319 );
320 }
321}