feat(detmir): add rust-first operations tooling
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
use std::fs::{self, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::Parser;
|
||||
use fs2::FileExt;
|
||||
use hayabusa_tools::{guess_host_from_filename, read_json_file};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const LOCK_PATH: &str = "/opt/hayabusa/state/aw-hayabusa-autoprocess.lock";
|
||||
const WRAPPER: &str = "/usr/local/bin/aw-hayabusa";
|
||||
const LINKER: &str = "/usr/local/bin/aw-hayabusa-link-case";
|
||||
const CASE_ALERT: &str = "/usr/local/bin/aw-hayabusa-case-alert";
|
||||
const LATEST_INTAKE: &str = "/opt/hayabusa/state/latest-intake.json";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(about = "Auto-process Hayabusa zip packages dropped onto aw-rus server")]
|
||||
struct Cli {
|
||||
#[arg(long, default_value = "/opt/activitywatch/aw-rus-ops/drop")]
|
||||
drop_dir: PathBuf,
|
||||
|
||||
#[arg(long, default_value_t = true)]
|
||||
once: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Sidecars {
|
||||
case_id: Option<i64>,
|
||||
host: Option<String>,
|
||||
mode: String,
|
||||
link_source: String,
|
||||
caseid_path: PathBuf,
|
||||
meta_path: PathBuf,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let code = match run() {
|
||||
Ok(code) => code,
|
||||
Err(err) => {
|
||||
eprintln!("{err:#}");
|
||||
1
|
||||
}
|
||||
};
|
||||
std::process::exit(code);
|
||||
}
|
||||
|
||||
fn run() -> Result<i32> {
|
||||
let cli = Cli::parse();
|
||||
let _once = cli.once;
|
||||
fs::create_dir_all(&cli.drop_dir)
|
||||
.with_context(|| format!("create {}", cli.drop_dir.display()))?;
|
||||
let lock_path = Path::new(LOCK_PATH);
|
||||
if let Some(parent) = lock_path.parent() {
|
||||
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||
}
|
||||
let lock = File::create(lock_path).with_context(|| format!("open {}", lock_path.display()))?;
|
||||
if lock.try_lock_exclusive().is_err() {
|
||||
eprintln!("autoprocess already running");
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let zips = list_zips(&cli.drop_dir)?;
|
||||
if zips.is_empty() {
|
||||
println!("no zip packages in drop dir");
|
||||
return Ok(0);
|
||||
}
|
||||
for zip_path in zips {
|
||||
let result = process_one(&zip_path)?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"processed": zip_path.display().to_string(),
|
||||
"latest_intake": result.latest_intake,
|
||||
"case_alert": result.case_alert,
|
||||
}))?
|
||||
);
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
struct ProcessResult {
|
||||
latest_intake: Value,
|
||||
case_alert: Option<Value>,
|
||||
}
|
||||
|
||||
fn list_zips(drop_dir: &Path) -> Result<Vec<PathBuf>> {
|
||||
let mut zips = Vec::new();
|
||||
for entry in fs::read_dir(drop_dir).with_context(|| format!("read {}", drop_dir.display()))? {
|
||||
let path = entry?.path();
|
||||
if path.extension().and_then(|ext| ext.to_str()) == Some("zip") {
|
||||
zips.push(path);
|
||||
}
|
||||
}
|
||||
zips.sort();
|
||||
Ok(zips)
|
||||
}
|
||||
|
||||
fn process_one(zip_path: &Path) -> Result<ProcessResult> {
|
||||
let sidecars = load_sidecars(zip_path)?;
|
||||
let host = guess_host(zip_path, &sidecars);
|
||||
let mode = if sidecars.mode.is_empty() {
|
||||
"incident".to_string()
|
||||
} else {
|
||||
sidecars.mode.clone()
|
||||
};
|
||||
|
||||
let mut accept_cmd = vec![
|
||||
"accept".to_string(),
|
||||
"--package".to_string(),
|
||||
zip_path.display().to_string(),
|
||||
];
|
||||
if let Some(host) = host {
|
||||
accept_cmd.extend(["--host".to_string(), host]);
|
||||
}
|
||||
run_checked(Path::new(WRAPPER), &accept_cmd)?;
|
||||
run_checked(
|
||||
Path::new(WRAPPER),
|
||||
&[
|
||||
"process-inbox".to_string(),
|
||||
"--mode".to_string(),
|
||||
mode.clone(),
|
||||
"--limit".to_string(),
|
||||
"1".to_string(),
|
||||
],
|
||||
)?;
|
||||
let latest = read_json_file(Path::new(LATEST_INTAKE))?;
|
||||
let report_dir = PathBuf::from(
|
||||
latest
|
||||
.get("report_dir")
|
||||
.and_then(Value::as_str)
|
||||
.context("latest intake report_dir missing")?,
|
||||
);
|
||||
let mut case_alert = None;
|
||||
if Path::new(CASE_ALERT).is_file() {
|
||||
let mut alert_cmd = vec![
|
||||
"--mode".to_string(),
|
||||
mode.clone(),
|
||||
"--link-source".to_string(),
|
||||
sidecars.link_source.clone(),
|
||||
];
|
||||
if let Some(case_id) = sidecars.case_id {
|
||||
alert_cmd.extend(["--case-id".to_string(), case_id.to_string()]);
|
||||
}
|
||||
let output = run_capture(Path::new(CASE_ALERT), &alert_cmd)?;
|
||||
case_alert = Some(json!({
|
||||
"returncode": output.returncode,
|
||||
"stdout": output.stdout.trim(),
|
||||
"stderr": output.stderr.trim(),
|
||||
}));
|
||||
}
|
||||
archive_sidecars(&report_dir, &sidecars)?;
|
||||
archive_drop_package(&report_dir, zip_path)?;
|
||||
if sidecars.case_id.is_some() && !Path::new(CASE_ALERT).is_file() {
|
||||
run_checked(
|
||||
Path::new(LINKER),
|
||||
&[
|
||||
"--case-id".to_string(),
|
||||
sidecars.case_id.unwrap().to_string(),
|
||||
"--mode".to_string(),
|
||||
mode,
|
||||
"--link-source".to_string(),
|
||||
sidecars.link_source,
|
||||
],
|
||||
)?;
|
||||
}
|
||||
Ok(ProcessResult {
|
||||
latest_intake: latest,
|
||||
case_alert,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_sidecars(zip_path: &Path) -> Result<Sidecars> {
|
||||
let base = zip_path.with_extension("");
|
||||
let caseid_path = base.with_extension("caseid");
|
||||
let meta_path = base.with_extension("meta.json");
|
||||
let meta = if meta_path.is_file() {
|
||||
read_json_file(&meta_path)?
|
||||
} else {
|
||||
json!({})
|
||||
};
|
||||
let mut case_id = meta.get("case_id").and_then(Value::as_i64);
|
||||
if case_id.is_none() && caseid_path.is_file() {
|
||||
let raw = fs::read_to_string(&caseid_path)
|
||||
.with_context(|| format!("read {}", caseid_path.display()))?;
|
||||
let trimmed = raw.trim();
|
||||
if !trimmed.is_empty() {
|
||||
case_id = Some(
|
||||
trimmed
|
||||
.parse::<i64>()
|
||||
.with_context(|| format!("parse {}", caseid_path.display()))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(Sidecars {
|
||||
case_id,
|
||||
host: meta.get("host").and_then(Value::as_str).map(str::to_string),
|
||||
mode: meta
|
||||
.get("mode")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("incident")
|
||||
.to_string(),
|
||||
link_source: meta
|
||||
.get("link_source")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("aw-rus-drop-autoprocess")
|
||||
.to_string(),
|
||||
caseid_path,
|
||||
meta_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn archive_sidecars(report_dir: &Path, sidecars: &Sidecars) -> Result<()> {
|
||||
let target_dir = report_dir.join("input-sidecars");
|
||||
fs::create_dir_all(&target_dir).with_context(|| format!("create {}", target_dir.display()))?;
|
||||
for path in [&sidecars.caseid_path, &sidecars.meta_path] {
|
||||
if path.is_file() {
|
||||
fs::rename(
|
||||
path,
|
||||
target_dir.join(path.file_name().context("sidecar file name")?),
|
||||
)
|
||||
.with_context(|| format!("move {}", path.display()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn archive_drop_package(report_dir: &Path, zip_path: &Path) -> Result<()> {
|
||||
let target_dir = report_dir.join("input-drop");
|
||||
fs::create_dir_all(&target_dir).with_context(|| format!("create {}", target_dir.display()))?;
|
||||
let target_path = target_dir.join(zip_path.file_name().context("zip file name")?);
|
||||
if target_path.exists() {
|
||||
fs::remove_file(&target_path)
|
||||
.with_context(|| format!("remove {}", target_path.display()))?;
|
||||
}
|
||||
fs::rename(zip_path, &target_path).with_context(|| format!("move {}", zip_path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn guess_host(zip_path: &Path, sidecars: &Sidecars) -> Option<String> {
|
||||
if let Some(host) = &sidecars.host {
|
||||
if !host.is_empty() {
|
||||
return Some(host.clone());
|
||||
}
|
||||
}
|
||||
let name = zip_path
|
||||
.file_stem()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(guess_host_from_filename)?;
|
||||
(!name.is_empty()).then_some(name)
|
||||
}
|
||||
|
||||
fn run_checked(program: &Path, args: &[String]) -> Result<()> {
|
||||
println!(
|
||||
"RUN {} {}",
|
||||
program.display(),
|
||||
args.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
);
|
||||
let status = Command::new(program)
|
||||
.args(args)
|
||||
.status()
|
||||
.with_context(|| format!("run {}", program.display()))?;
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"{} failed with status {}",
|
||||
program.display(),
|
||||
status.code().unwrap_or(1)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Captured {
|
||||
returncode: i32,
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
}
|
||||
|
||||
fn run_capture(program: &Path, args: &[String]) -> Result<Captured> {
|
||||
println!(
|
||||
"RUN {} {}",
|
||||
program.display(),
|
||||
args.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
);
|
||||
let output = Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.with_context(|| format!("run {}", program.display()))?;
|
||||
Ok(Captured {
|
||||
returncode: output.status.code().unwrap_or(1),
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use hayabusa_tools::{
|
||||
analyze_report, build_case_payload, build_comment, build_hayabusa_payload, build_telegram_text,
|
||||
env_bool, env_string, http_client, normalize_case_api_base, patch_json, post_json,
|
||||
read_json_file, required_str, severity_meets,
|
||||
};
|
||||
use reqwest::blocking::Client;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
about = "Auto-create/update AW-rus case, compute Hayabusa severity, and send Telegram alerts"
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(long)]
|
||||
case_id: Option<i64>,
|
||||
|
||||
#[arg(long, default_value = "/opt/hayabusa/state/latest-intake.json")]
|
||||
intake_json: PathBuf,
|
||||
|
||||
#[arg(long)]
|
||||
case_api_base: Option<String>,
|
||||
|
||||
#[arg(long, default_value = "incident")]
|
||||
mode: String,
|
||||
|
||||
#[arg(long, default_value = "aw-rus-drop-autoprocess")]
|
||||
link_source: String,
|
||||
|
||||
#[arg(long, default_value_t = false)]
|
||||
auto_create: bool,
|
||||
|
||||
#[arg(long)]
|
||||
auto_create_min_severity: Option<String>,
|
||||
|
||||
#[arg(long, default_value_t = false)]
|
||||
telegram_enabled: bool,
|
||||
|
||||
#[arg(long)]
|
||||
telegram_min_severity: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
telegram_bot_token: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
telegram_chat_ids: Option<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let code = match run() {
|
||||
Ok(code) => code,
|
||||
Err(err) => {
|
||||
eprintln!("{err:#}");
|
||||
1
|
||||
}
|
||||
};
|
||||
std::process::exit(code);
|
||||
}
|
||||
|
||||
fn run() -> Result<i32> {
|
||||
let cli = Cli::parse();
|
||||
let intake = read_json_file(&cli.intake_json)?;
|
||||
let summary = analyze_report(PathBuf::from(required_str(&intake, "report_dir")?).as_path())?;
|
||||
let case_api_base = normalize_case_api_base(cli.case_api_base.as_deref().unwrap_or(
|
||||
&env_string("AW_HAYABUSA_CASE_API_BASE", "http://127.0.0.1:5602"),
|
||||
));
|
||||
let auto_create = cli.auto_create || env_bool("AW_HAYABUSA_AUTO_CASE_ENABLED", true);
|
||||
let auto_create_min_severity = cli
|
||||
.auto_create_min_severity
|
||||
.unwrap_or_else(|| env_string("AW_HAYABUSA_AUTO_CASE_MIN_SEVERITY", "medium"));
|
||||
let telegram_enabled = cli.telegram_enabled || env_bool("AW_HAYABUSA_TELEGRAM_ENABLED", false);
|
||||
let telegram_min_severity = cli
|
||||
.telegram_min_severity
|
||||
.unwrap_or_else(|| env_string("AW_HAYABUSA_TELEGRAM_MIN_SEVERITY", "high"));
|
||||
let telegram_bot_token = cli
|
||||
.telegram_bot_token
|
||||
.unwrap_or_else(|| env_string("AW_HAYABUSA_TELEGRAM_BOT_TOKEN", ""));
|
||||
let telegram_chat_ids = cli
|
||||
.telegram_chat_ids
|
||||
.unwrap_or_else(|| env_string("AW_HAYABUSA_TELEGRAM_CHAT_IDS", ""));
|
||||
let client = http_client()?;
|
||||
|
||||
let mut case_id = cli.case_id;
|
||||
let mut created_case = Value::Null;
|
||||
let mut case_error: Option<String> = None;
|
||||
let mut linked = false;
|
||||
let mut comment_added = false;
|
||||
|
||||
if let Err(err) = (|| -> Result<()> {
|
||||
if case_id.is_none()
|
||||
&& auto_create
|
||||
&& severity_meets(&summary.severity, &auto_create_min_severity)
|
||||
{
|
||||
created_case = post_json(
|
||||
&client,
|
||||
&format!("{case_api_base}/api/0/dlp/cases"),
|
||||
&build_case_payload(&intake, &summary)?,
|
||||
)?;
|
||||
case_id = created_case.get("id").and_then(Value::as_i64);
|
||||
}
|
||||
if let Some(id) = case_id {
|
||||
patch_json(
|
||||
&client,
|
||||
&format!("{case_api_base}/api/0/dlp/cases/{id}"),
|
||||
&json!({"severity": summary.severity}),
|
||||
)?;
|
||||
post_json(
|
||||
&client,
|
||||
&format!("{case_api_base}/api/0/dlp/cases/{id}/forensics/hayabusa"),
|
||||
&build_hayabusa_payload(&intake, &cli.mode, &cli.link_source)?,
|
||||
)?;
|
||||
linked = true;
|
||||
post_json(
|
||||
&client,
|
||||
&format!("{case_api_base}/api/0/dlp/cases/{id}/comments"),
|
||||
&json!({"comment": build_comment(&summary, &intake)?, "author": "aw-hayabusa-auto"}),
|
||||
)?;
|
||||
comment_added = true;
|
||||
}
|
||||
Ok(())
|
||||
})() {
|
||||
case_error = Some(err.to_string());
|
||||
}
|
||||
|
||||
let telegram_results = if telegram_enabled
|
||||
&& !telegram_bot_token.is_empty()
|
||||
&& severity_meets(&summary.severity, &telegram_min_severity)
|
||||
{
|
||||
let chat_ids = telegram_chat_ids
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|item| !item.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
send_telegram(
|
||||
&client,
|
||||
&telegram_bot_token,
|
||||
&chat_ids,
|
||||
&build_telegram_text(case_id, &intake, &summary)?,
|
||||
)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let result = json!({
|
||||
"summary": summary,
|
||||
"case_id": case_id,
|
||||
"case_created": if created_case.is_null() { Value::Null } else { created_case },
|
||||
"case_linked": linked,
|
||||
"case_comment_added": comment_added,
|
||||
"case_error": case_error,
|
||||
"telegram_results": telegram_results,
|
||||
});
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
Ok(if result.get("case_error").is_some_and(|v| !v.is_null()) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
})
|
||||
}
|
||||
|
||||
fn send_telegram(client: &Client, bot_token: &str, chat_ids: &[&str], text: &str) -> Vec<Value> {
|
||||
let mut results = Vec::new();
|
||||
for chat_id in chat_ids {
|
||||
let url = format!("https://api.telegram.org/bot{bot_token}/sendMessage");
|
||||
let response = client
|
||||
.post(&url)
|
||||
.form(&[("chat_id", *chat_id), ("text", text)])
|
||||
.send();
|
||||
match response {
|
||||
Ok(resp) => match resp.json::<Value>() {
|
||||
Ok(body) => results.push(json!({"chat_id": chat_id, "ok": true, "response": body})),
|
||||
Err(err) => {
|
||||
results.push(json!({"chat_id": chat_id, "ok": false, "error": err.to_string()}))
|
||||
}
|
||||
},
|
||||
Err(_) => results
|
||||
.push(json!({"chat_id": chat_id, "ok": false, "error": "telegram request failed"})),
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::Parser;
|
||||
use hayabusa_tools::{guess_host_from_filename, windows_filename};
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
|
||||
const WINDOWS_EXPORT_CMD: &str = r"powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\AWatch-rus\export-evtx-for-hayabusa.ps1 -DaysBack {days_back} | ConvertTo-Json -Depth 8 -Compress";
|
||||
const WINDOWS_LATEST_ZIP_CMD: &str = r"Get-ChildItem 'C:\ProgramData\AWatch-rus\forensics\evtx-exports' -File -Filter '*.zip' | Sort-Object LastWriteTime -Descending | Select-Object -First 1 FullName,Length,LastWriteTime | ConvertTo-Json -Compress";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
about = "Run Windows EVTX export and Hayabusa intake directly from aw-server, without the laptop"
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "/opt/activitywatch/aw-rus-ops/ansible/inventory.ini"
|
||||
)]
|
||||
inventory: PathBuf,
|
||||
|
||||
#[arg(long, default_value = "/opt/activitywatch/aw-rus-ops/venv/bin/ansible")]
|
||||
ansible_bin: PathBuf,
|
||||
|
||||
#[arg(long, default_value = "/opt/activitywatch/aw-rus-ops/drop")]
|
||||
drop_dir: PathBuf,
|
||||
|
||||
#[arg(long, default_value_t = 1)]
|
||||
days_back: i64,
|
||||
|
||||
#[arg(long, default_value = "incident", value_parser = ["quick", "incident", "full"])]
|
||||
mode: String,
|
||||
|
||||
#[arg(long)]
|
||||
case_id: Option<i64>,
|
||||
|
||||
#[arg(long, default_value = "aw-rus-ops-from-windows")]
|
||||
link_source: String,
|
||||
|
||||
#[arg(long, default_value = "aw_windows")]
|
||||
windows_group: String,
|
||||
|
||||
#[arg(long, default_value = "/usr/local/bin/aw-hayabusa")]
|
||||
wrapper: PathBuf,
|
||||
|
||||
#[arg(long, default_value = "/usr/local/bin/aw-hayabusa-link-case")]
|
||||
linker: PathBuf,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("{err:#}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
ensure_file(&cli.inventory, "inventory")?;
|
||||
ensure_file(&cli.ansible_bin, "ansible binary")?;
|
||||
ensure_file(&cli.wrapper, "wrapper")?;
|
||||
fs::create_dir_all(&cli.drop_dir)
|
||||
.with_context(|| format!("create {}", cli.drop_dir.display()))?;
|
||||
|
||||
let export_arg = WINDOWS_EXPORT_CMD.replace("{days_back}", &cli.days_back.to_string());
|
||||
let export_cmd = vec![
|
||||
cli.windows_group.as_str(),
|
||||
"-i",
|
||||
path_str(&cli.inventory)?,
|
||||
"-m",
|
||||
"win_shell",
|
||||
"-a",
|
||||
export_arg.as_str(),
|
||||
];
|
||||
print_run("RUN_EXPORT", &cli.ansible_bin, &export_cmd);
|
||||
let export_out = run_capture(&cli.ansible_bin, &export_cmd)?;
|
||||
let export_json = extract_json_blob(&export_out)?;
|
||||
|
||||
let list_cmd = vec![
|
||||
cli.windows_group.as_str(),
|
||||
"-i",
|
||||
path_str(&cli.inventory)?,
|
||||
"-m",
|
||||
"win_shell",
|
||||
"-a",
|
||||
WINDOWS_LATEST_ZIP_CMD,
|
||||
];
|
||||
print_run("RUN_LIST", &cli.ansible_bin, &list_cmd);
|
||||
let latest_out = run_capture(&cli.ansible_bin, &list_cmd)?;
|
||||
let latest = normalize_latest_zip(extract_json_blob(&latest_out)?)?;
|
||||
let remote_zip = latest
|
||||
.get("FullName")
|
||||
.and_then(Value::as_str)
|
||||
.context("latest zip FullName missing")?;
|
||||
let filename = windows_filename(remote_zip);
|
||||
let local_zip = cli.drop_dir.join(&filename);
|
||||
let remote_zip_posix = remote_zip.replace('\\', "/");
|
||||
let fetch_arg = format!(
|
||||
"src={} dest={}/ flat=yes",
|
||||
remote_zip_posix,
|
||||
cli.drop_dir.display()
|
||||
);
|
||||
let fetch_cmd = vec![
|
||||
cli.windows_group.as_str(),
|
||||
"-i",
|
||||
path_str(&cli.inventory)?,
|
||||
"-m",
|
||||
"fetch",
|
||||
"-a",
|
||||
fetch_arg.as_str(),
|
||||
];
|
||||
print_run("RUN_FETCH", &cli.ansible_bin, &fetch_cmd);
|
||||
run_checked(&cli.ansible_bin, &fetch_cmd)?;
|
||||
if !local_zip.is_file() {
|
||||
bail!("fetched zip not found: {}", local_zip.display());
|
||||
}
|
||||
|
||||
let host = export_json
|
||||
.get("hostname")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| guess_host_from_filename(&filename));
|
||||
let mut accept_args = vec![
|
||||
"accept".to_string(),
|
||||
"--package".to_string(),
|
||||
local_zip.display().to_string(),
|
||||
];
|
||||
if !host.is_empty() {
|
||||
accept_args.extend(["--host".to_string(), host]);
|
||||
}
|
||||
print_run_owned("RUN_ACCEPT", &cli.wrapper, &accept_args);
|
||||
run_checked_owned(&cli.wrapper, &accept_args)?;
|
||||
|
||||
let process_args = vec![
|
||||
"process-inbox".to_string(),
|
||||
"--mode".to_string(),
|
||||
cli.mode.clone(),
|
||||
"--limit".to_string(),
|
||||
"1".to_string(),
|
||||
];
|
||||
print_run_owned("RUN_PROCESS", &cli.wrapper, &process_args);
|
||||
run_checked_owned(&cli.wrapper, &process_args)?;
|
||||
|
||||
if let Some(case_id) = cli.case_id {
|
||||
ensure_file(&cli.linker, "linker")?;
|
||||
let link_args = vec![
|
||||
"--case-id".to_string(),
|
||||
case_id.to_string(),
|
||||
"--mode".to_string(),
|
||||
cli.mode,
|
||||
"--link-source".to_string(),
|
||||
cli.link_source,
|
||||
];
|
||||
print_run_owned("RUN_LINK", &cli.linker, &link_args);
|
||||
run_checked_owned(&cli.linker, &link_args)?;
|
||||
}
|
||||
|
||||
let latest_intake = Path::new("/opt/hayabusa/state/latest-intake.json");
|
||||
println!("LATEST_INTAKE");
|
||||
println!(
|
||||
"{}",
|
||||
fs::read_to_string(latest_intake).context("read latest intake")?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_file(path: &Path, label: &str) -> Result<()> {
|
||||
if !path.is_file() {
|
||||
bail!("{label} not found: {}", path.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn path_str(path: &Path) -> Result<&str> {
|
||||
path.to_str()
|
||||
.with_context(|| format!("path is not UTF-8: {}", path.display()))
|
||||
}
|
||||
|
||||
fn run_capture(program: &Path, args: &[&str]) -> Result<String> {
|
||||
let output = Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.with_context(|| format!("run {}", program.display()))?;
|
||||
if !output.status.success() {
|
||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
std::process::exit(output.status.code().unwrap_or(1));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
fn run_checked(program: &Path, args: &[&str]) -> Result<()> {
|
||||
let status = Command::new(program)
|
||||
.args(args)
|
||||
.status()
|
||||
.with_context(|| format!("run {}", program.display()))?;
|
||||
if !status.success() {
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_checked_owned(program: &Path, args: &[String]) -> Result<()> {
|
||||
let status = Command::new(program)
|
||||
.args(args)
|
||||
.status()
|
||||
.with_context(|| format!("run {}", program.display()))?;
|
||||
if !status.success() {
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_run(label: &str, program: &Path, args: &[&str]) {
|
||||
println!("{label} {} {}", program.display(), args.join(" "));
|
||||
}
|
||||
|
||||
fn print_run_owned(label: &str, program: &Path, args: &[String]) {
|
||||
println!("{label} {} {}", program.display(), args.join(" "));
|
||||
}
|
||||
|
||||
fn extract_json_blob(text: &str) -> Result<Value> {
|
||||
let re = Regex::new(r"(?s)(\{.*\}|\[.*\])").context("compile JSON extractor")?;
|
||||
let matches = re.find_iter(text).collect::<Vec<_>>();
|
||||
for candidate in matches.iter().rev().map(|item| item.as_str()) {
|
||||
if let Ok(value) = serde_json::from_str::<Value>(candidate) {
|
||||
return Ok(value);
|
||||
}
|
||||
}
|
||||
bail!("cannot parse JSON from ansible output:\n{text}")
|
||||
}
|
||||
|
||||
fn normalize_latest_zip(value: Value) -> Result<Value> {
|
||||
if let Some(items) = value.as_array() {
|
||||
items.first().cloned().context("latest zip list is empty")
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use hayabusa_tools::{build_hayabusa_payload, http_client, link_hayabusa_to_case, read_json_file};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(about = "Link Hayabusa intake metadata to AW-rus case management")]
|
||||
struct Cli {
|
||||
#[arg(long)]
|
||||
case_id: i64,
|
||||
|
||||
#[arg(long, default_value = "/opt/hayabusa/state/latest-intake.json")]
|
||||
intake_json: PathBuf,
|
||||
|
||||
#[arg(long, default_value = "http://127.0.0.1:5602")]
|
||||
case_api_base: String,
|
||||
|
||||
#[arg(long, default_value = "incident")]
|
||||
mode: String,
|
||||
|
||||
#[arg(long, default_value = "aw-rus-ops")]
|
||||
link_source: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("{err:#}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let intake = read_json_file(&cli.intake_json)?;
|
||||
let client = http_client()?;
|
||||
let case = link_hayabusa_to_case(
|
||||
&client,
|
||||
&cli.case_api_base,
|
||||
cli.case_id,
|
||||
&intake,
|
||||
&cli.mode,
|
||||
&cli.link_source,
|
||||
)?;
|
||||
let _payload = build_hayabusa_payload(&intake, &cli.mode, &cli.link_source)?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"case_id": cli.case_id,
|
||||
"intake": intake,
|
||||
"forensics": case.get("forensics").cloned().unwrap_or(serde_json::Value::Null),
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user