use crate::cli::last_trigger;
use tirith_core::engine::{self, AnalysisContext};
use tirith_core::escalation::CallerContext;
use tirith_core::extract::ScanContext;
use tirith_core::output;
use tirith_core::threatdb_api::RuntimeThreatMode;
use tirith_core::tokenize::ShellType;
use tirith_core::verdict::{upgraded_action_from_findings, Action};
#[allow(clippy::too_many_arguments)]
pub fn run(
cmd: &str,
shell: &str,
json: bool,
non_interactive: bool,
interactive_flag: bool,
approval_check: bool,
strict_warn: bool,
no_daemon: bool,
warn_only: bool,
) -> i32 {
if cmd.trim().is_empty() {
if approval_check {
match tirith_core::approval::write_no_approval_file() {
Ok(path) => {
println!("{}", path.display());
return 0;
}
Err(e) => {
eprintln!("tirith: failed to write approval file: {e}");
return 1;
}
}
}
return 0;
}
let shell_type = match shell.parse::<ShellType>() {
Ok(s) => s,
Err(_) => {
eprintln!("tirith: warning: unknown shell '{shell}', falling back to posix");
ShellType::Posix
}
};
let interactive = if interactive_flag {
true
} else if non_interactive {
false
} else if let Ok(val) = std::env::var("TIRITH_INTERACTIVE") {
val == "1"
} else {
is_terminal::is_terminal(std::io::stderr())
};
let cwd = std::env::current_dir()
.ok()
.map(|p| p.display().to_string());
let bypass_requested = std::env::var("TIRITH")
.ok()
.map(|v| v == "0")
.unwrap_or(false);
crate::cli::threatdb_cmd::maybe_background_update();
let session_id = tirith_core::session::resolve_session_id();
let (mut raw_verdict, engine_policy) = if !approval_check && !no_daemon {
if let Some(resp) =
crate::cli::daemon::try_daemon_check(cmd, shell, cwd.as_deref(), interactive)
{
if let Some(ref raw_findings) = resp.raw_findings {
let raw_action_parsed = resp
.raw_action
.as_deref()
.and_then(parse_action)
.unwrap_or(resp.action);
(
tirith_core::verdict::Verdict {
action: raw_action_parsed,
findings: raw_findings.clone(),
tier_reached: resp.tier_reached,
bypass_requested,
bypass_honored: resp.bypass_honored,
bypass_available: resp.bypass_available,
interactive_detected: interactive,
policy_path_used: resp.policy_path_used,
timings_ms: resp.timings_ms,
urls_extracted_count: resp.urls_extracted_count,
requires_approval: None,
approval_timeout_secs: None,
approval_fallback: None,
approval_rule: None,
approval_description: None,
escalation_reason: None,
},
None,
)
} else {
eprintln!(
"tirith: daemon does not support raw findings — falling back to local analysis"
);
let ctx = AnalysisContext {
input: cmd.to_string(),
shell: shell_type,
scan_context: ScanContext::Exec,
raw_bytes: None,
interactive,
cwd: cwd.clone(),
file_path: None,
repo_root: None,
is_config_override: false,
clipboard_html: None,
};
let (v, p) = engine::analyze_returning_policy(&ctx);
(v, Some(p))
}
} else {
let ctx = AnalysisContext {
input: cmd.to_string(),
shell: shell_type,
scan_context: ScanContext::Exec,
raw_bytes: None,
interactive,
cwd: cwd.clone(),
file_path: None,
repo_root: None,
is_config_override: false,
clipboard_html: None,
};
let (v, p) = engine::analyze_returning_policy(&ctx);
(v, Some(p))
}
} else {
let ctx = AnalysisContext {
input: cmd.to_string(),
shell: shell_type,
scan_context: ScanContext::Exec,
raw_bytes: None,
interactive,
cwd: cwd.clone(),
file_path: None,
repo_root: None,
is_config_override: false,
clipboard_html: None,
};
let (v, p) = engine::analyze_returning_policy(&ctx);
(v, Some(p))
};
if raw_verdict.bypass_honored {
let policy =
engine_policy.unwrap_or_else(|| tirith_core::policy::Policy::discover(cwd.as_deref()));
let event_id = uuid::Uuid::new_v4().to_string();
tirith_core::audit::log_verdict(
&raw_verdict,
cmd,
None,
Some(event_id),
&policy.dlp_custom_patterns,
);
return 0;
}
let ran_locally = engine_policy.is_some();
let policy =
engine_policy.unwrap_or_else(|| tirith_core::policy::Policy::discover(cwd.as_deref()));
if ran_locally {
let runtime_findings = tirith_core::threatdb_api::enrich_command(
cmd,
shell_type,
&policy.threat_intel,
RuntimeThreatMode::Inline,
);
if !runtime_findings.is_empty() {
raw_verdict.findings.extend(runtime_findings);
raw_verdict.action =
upgraded_action_from_findings(&raw_verdict.findings, raw_verdict.action);
}
}
let raw_action_str = format!("{:?}", raw_verdict.action);
let raw_rule_ids: Vec<String> = raw_verdict
.findings
.iter()
.map(|f| f.rule_id.to_string())
.collect();
let effective = tirith_core::escalation::post_process_verdict(
&raw_verdict,
&policy,
cmd,
&session_id,
CallerContext::Cli,
);
let event_id = uuid::Uuid::new_v4().to_string();
tirith_core::audit::log_verdict_with_raw(
&effective,
cmd,
None,
Some(event_id),
&policy.dlp_custom_patterns,
Some(raw_action_str),
Some(raw_rule_ids),
);
if approval_check {
if effective.requires_approval == Some(true) {
let meta = tirith_core::approval::ApprovalMetadata {
requires_approval: true,
timeout_secs: effective.approval_timeout_secs.unwrap_or(0),
fallback: effective
.approval_fallback
.clone()
.unwrap_or_else(|| "block".to_string()),
rule_id: effective
.approval_rule
.clone()
.unwrap_or_else(|| "unknown".to_string()),
description: effective.approval_description.clone().unwrap_or_default(),
};
match tirith_core::approval::write_approval_file(&meta) {
Ok(path) => {
println!("{}", path.display());
}
Err(e) => {
eprintln!("tirith: failed to write approval file: {e}");
return 1;
}
}
} else {
match tirith_core::approval::write_no_approval_file() {
Ok(path) => {
println!("{}", path.display());
}
Err(e) => {
eprintln!("tirith: failed to write approval file: {e}");
return 1;
}
}
}
}
if interactive
&& effective.action != Action::Block
&& tirith_core::checkpoint::should_auto_checkpoint(cmd)
{
if let Some(cwd_val) = &cwd {
let cwd_owned = cwd_val.clone();
let cmd_owned = cmd.to_string();
std::thread::spawn(move || {
if let Err(e) =
tirith_core::checkpoint::create(&[cwd_owned.as_str()], Some(&cmd_owned))
{
eprintln!("tirith: auto-checkpoint failed (non-fatal): {e}");
} else {
let config = tirith_core::checkpoint::CheckpointConfig::default();
if let Err(e) = tirith_core::checkpoint::purge(&config) {
eprintln!("tirith: checkpoint purge failed (non-fatal): {e}");
}
}
});
}
}
if effective.action != Action::Allow {
last_trigger::write_last_trigger(&effective, cmd, &policy.dlp_custom_patterns);
}
if !policy.webhooks.is_empty() {
tirith_core::webhook::dispatch(
&effective,
cmd,
&policy.webhooks,
&policy.dlp_custom_patterns,
);
}
if approval_check {
if output::write_human(&effective, warn_only, std::io::stderr().lock()).is_err() {
eprintln!("tirith: failed to write approval output");
}
if effective.action == Action::Warn && (strict_warn || policy.strict_warn) {
let max_sev = effective
.findings
.iter()
.map(|f| f.severity)
.max()
.unwrap_or(tirith_core::verdict::Severity::Low);
match tirith_core::approval::write_warn_ack_file(effective.findings.len(), &max_sev) {
Ok(path) => {
println!("{}", path.display());
}
Err(e) => {
eprintln!("tirith: failed to write warn-ack file: {e}");
return 1;
}
}
return tirith_core::verdict::Action::WarnAck.exit_code();
}
return effective.action.exit_code();
}
if json {
if output::write_json(
&effective,
&policy.dlp_custom_patterns,
std::io::stdout().lock(),
)
.is_err()
{
eprintln!("tirith: failed to write JSON output");
}
} else if output::write_human_auto(&effective, warn_only).is_err() {
eprintln!("tirith: failed to write output");
}
let exit_code = effective.action.exit_code();
if exit_code == 2 && (strict_warn || policy.strict_warn) && interactive {
eprint!(
"tirith: proceed with {} warning(s)? [y/N] ",
effective.findings.len()
);
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
if matches!(input.trim(), "y" | "Y" | "yes" | "Yes") {
return 0;
}
return 1;
}
exit_code
}
fn parse_action(s: &str) -> Option<Action> {
match s {
"Allow" => Some(Action::Allow),
"Warn" => Some(Action::Warn),
"WarnAck" => Some(Action::WarnAck),
"Block" => Some(Action::Block),
_ => None,
}
}