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