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