Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2ac9353
refactor: remove certifried automation and update comments for clarity
l50 May 17, 2026
eb6c984
refactor: restrict error pattern matching to structured tool_outputs …
l50 May 17, 2026
6513bdd
refactor: remove legacy scalar output support from payload processing
l50 May 17, 2026
ca2a4a7
refactor: remove legacy scalar output handling from result processing
l50 May 17, 2026
b5a4439
refactor: replace multi-argument functions with parameter structs for…
l50 May 17, 2026
50ba3ee
style: reformat basic_conn function signature for readability
l50 May 18, 2026
943b5f0
refactor: remove dead code and improve test-only annotation usage
l50 May 18, 2026
eb36eff
feat: add atomic deferred queue counter management with Redis Lua scr…
l50 May 18, 2026
966d0f4
chore: enforce clippy lints and unify lint configuration across works…
l50 May 18, 2026
b2fbf26
refactor: remove outdated python parity comments from orchestrator mo…
l50 May 18, 2026
12b1a23
refactor: remove python-matching doc references and clarify comments
l50 May 18, 2026
416c07f
refactor: remove legacy python references and clarify rust-native log…
l50 May 18, 2026
403c854
refactor: fix needless_collect / redundant_clone / manual_let_else / …
l50 May 18, 2026
657ca71
refactor: avoid intermediate Vec in adcs credential selection logic
l50 May 18, 2026
acf76ae
test: add comprehensive unit tests for env var and outcome helpers
l50 May 18, 2026
ea61d3e
test: add comprehensive tests for domain controller discovery and par…
l50 May 18, 2026
874a3da
style: reformat test code for readability and consistency
l50 May 18, 2026
e2bc57f
test: add comprehensive tests for connection and target extraction he…
l50 May 18, 2026
5e285e9
test: fix unused variable warning in extract_target_from_host_key test
l50 May 18, 2026
a767b78
test: add comprehensive unit tests for detection, output extraction, …
l50 May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@
resolver = "2"
members = ["ares-core", "ares-cli", "ares-llm", "ares-tools"]

[workspace.lints.clippy]
# Functions with many parameters must use a parameter struct (see the
# `*Params` / `*Config` types throughout the workspace). Suppressing this
# with `#[allow(...)]` defeats the whole point — fix the signature instead.
too_many_arguments = "deny"
# Prefer `let ... else { ... }` over `let x = match opt { Some(x) => x, None => ... };`
# — same semantics, fewer lines, no rightward drift.
manual_let_else = "deny"
# `.iter().filter(..).collect::<Vec<_>>().len()` / `.is_empty()` / `.contains(..)` —
# allocate-then-consume when the iterator already answers the question.
needless_collect = "deny"
# `.clone()` on a value whose last use immediately follows — the move would suffice.
redundant_clone = "deny"
# Types that derive `PartialEq` should derive `Eq` too when their fields permit.
# When they don't (e.g. an `f64` or `serde_json::Value` field), suppress with
# `#[expect(clippy::derive_partial_eq_without_eq, reason = "...")]` on the type
# explaining which field blocks it.
derive_partial_eq_without_eq = "deny"

[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
3 changes: 3 additions & 0 deletions ares-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ tokio = { workspace = true }
rstest = "0.26"
tempfile = "3"
ares-core = { path = "../ares-core", features = ["test-utils", "blue", "telemetry"] }

[lints]
workspace = true
10 changes: 5 additions & 5 deletions ares-cli/src/blue/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,17 @@ pub(crate) async fn run_blue(cmd: BlueCommands, redis_url: Option<String>) -> Re
grafana_url,
grafana_api_key,
} => {
submit::blue_submit(
submit::blue_submit(submit::BlueSubmitParams {
redis_url,
alert_json,
investigation_id,
model,
max_steps,
multi_agent,
!no_auto_route,
auto_route: !no_auto_route,
grafana_url,
grafana_api_key,
)
})
.await
}
BlueCommands::Watch {
Expand Down Expand Up @@ -129,15 +129,15 @@ pub(crate) async fn run_blue(cmd: BlueCommands, redis_url: Option<String>) -> Re
grafana_url,
grafana_api_key,
} => {
submit::blue_from_operation(
submit::blue_from_operation(submit::BlueFromOperationParams {
redis_url,
operation_id,
latest,
model,
max_steps,
grafana_url,
grafana_api_key,
)
})
.await
}
}
Expand Down
69 changes: 46 additions & 23 deletions ares-cli/src/blue/submit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,31 @@ use ares_core::state::RedisStateReader;
use crate::ops::submit::{collect_env_vars, resolve_model, BLUE_ENV_VAR_NAMES};
use crate::redis_conn::{connect_redis, resolve_operation_id};

#[allow(clippy::too_many_arguments)]
pub(crate) async fn blue_submit(
redis_url: Option<String>,
alert_json: String,
investigation_id: Option<String>,
model: Option<String>,
max_steps: u32,
multi_agent: bool,
auto_route: bool,
grafana_url: Option<String>,
grafana_api_key: Option<String>,
) -> Result<()> {
pub(crate) struct BlueSubmitParams {
pub redis_url: Option<String>,
pub alert_json: String,
pub investigation_id: Option<String>,
pub model: Option<String>,
pub max_steps: u32,
pub multi_agent: bool,
pub auto_route: bool,
pub grafana_url: Option<String>,
pub grafana_api_key: Option<String>,
}

pub(crate) async fn blue_submit(p: BlueSubmitParams) -> Result<()> {
let BlueSubmitParams {
redis_url,
alert_json,
investigation_id,
model,
max_steps,
multi_agent,
auto_route,
grafana_url,
grafana_api_key,
} = p;

let alert: serde_json::Value = if std::path::Path::new(&alert_json).is_file() {
let content = std::fs::read_to_string(&alert_json)
.with_context(|| format!("Failed to read alert file: {alert_json}"))?;
Expand Down Expand Up @@ -49,7 +62,6 @@ pub(crate) async fn blue_submit(

let now = Utc::now();

// Format must match Python blue_orchestrator_client.py
let request = serde_json::json!({
"investigation_id": inv_id,
"alert": alert,
Expand Down Expand Up @@ -90,16 +102,27 @@ pub(crate) async fn blue_submit(
Ok(())
}

#[allow(clippy::too_many_arguments)]
pub(crate) async fn blue_from_operation(
redis_url: Option<String>,
operation_id: Option<String>,
latest: bool,
model: Option<String>,
max_steps: u32,
grafana_url: Option<String>,
grafana_api_key: Option<String>,
) -> Result<()> {
pub(crate) struct BlueFromOperationParams {
pub redis_url: Option<String>,
pub operation_id: Option<String>,
pub latest: bool,
pub model: Option<String>,
pub max_steps: u32,
pub grafana_url: Option<String>,
pub grafana_api_key: Option<String>,
}

pub(crate) async fn blue_from_operation(p: BlueFromOperationParams) -> Result<()> {
let BlueFromOperationParams {
redis_url,
operation_id,
latest,
model,
max_steps,
grafana_url,
grafana_api_key,
} = p;

let mut conn = connect_redis(redis_url.clone()).await?;
let op_id = resolve_operation_id(&mut conn, operation_id, latest).await?;

Expand Down
16 changes: 8 additions & 8 deletions ares-cli/src/blue/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ pub(crate) async fn blue_watch(
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S");
println!("[{now}] Submitting blue investigation from latest operation...");

match super::submit::blue_from_operation(
redis_url.clone(),
None,
true, // --latest
model.clone(),
match super::submit::blue_from_operation(super::submit::BlueFromOperationParams {
redis_url: redis_url.clone(),
operation_id: None,
latest: true,
model: model.clone(),
max_steps,
grafana_url.clone(),
grafana_api_key.clone(),
)
grafana_url: grafana_url.clone(),
grafana_api_key: grafana_api_key.clone(),
})
.await
{
Ok(()) => info!("Investigation submitted successfully"),
Expand Down
2 changes: 1 addition & 1 deletion ares-cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pub(crate) enum Commands {
#[arg(hide = true)]
_role: Option<String>,

/// Accept and ignore legacy Python-style --worker-args.* flags
/// Accept and ignore legacy `--worker-args.*` flags
#[arg(long = "worker-args.redis-url", hide = true)]
_legacy_redis_url: Option<String>,
},
Expand Down
2 changes: 1 addition & 1 deletion ares-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ fn config_set_model(

if all {
// Replace model for all agents
let mut new_contents = contents.clone();
let mut new_contents = contents;
for (role_name, agent) in &cfg.agents {
new_contents = replace_model_in_yaml(&new_contents, role_name, &agent.model, &model);
}
Expand Down
6 changes: 3 additions & 3 deletions ares-cli/src/detection/playbook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,12 @@ mod tests {
.collect();
assert_eq!(ip_targets.len(), 1);
assert_eq!(ip_targets[0].value, "192.168.58.10");
let hostname_targets: Vec<_> = playbook
let hostname_count = playbook
.detection_targets
.iter()
.filter(|t| t.ioc_type == "hostname")
.collect();
assert_eq!(hostname_targets.len(), 1);
.count();
assert_eq!(hostname_count, 1);
}

#[test]
Expand Down
86 changes: 86 additions & 0 deletions ares-cli/src/detection/techniques/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,28 @@ use ares_core::models::{Credential, Host, Share, SharedRedTeamState};
fn get_technique_name_known() {
assert_eq!(get_technique_name("T1046"), "Network Service Discovery");
assert_eq!(get_technique_name("T1003"), "OS Credential Dumping");
assert_eq!(get_technique_name("T1003.001"), "LSASS Memory");
assert_eq!(get_technique_name("T1003.006"), "DCSync");
assert_eq!(get_technique_name("T1078"), "Valid Accounts");
assert_eq!(get_technique_name("T1078.002"), "Domain Accounts");
assert_eq!(get_technique_name("T1110"), "Brute Force");
assert_eq!(
get_technique_name("T1558"),
"Steal or Forge Kerberos Tickets"
);
assert_eq!(get_technique_name("T1558.001"), "Golden Ticket");
assert_eq!(get_technique_name("T1558.003"), "Kerberoasting");
assert_eq!(get_technique_name("T1558.004"), "AS-REP Roasting");
assert_eq!(get_technique_name("T1021"), "Remote Services");
assert_eq!(get_technique_name("T1021.002"), "SMB/Windows Admin Shares");
assert_eq!(get_technique_name("T1649"), "ADCS Certificate Theft");
assert_eq!(
get_technique_name("T1550"),
"Use Alternate Authentication Material"
);
assert_eq!(get_technique_name("T1550.002"), "Pass the Hash");
assert_eq!(get_technique_name("T1484"), "Domain Policy Modification");
assert_eq!(get_technique_name("T1087"), "Account Discovery");
}

#[test]
Expand Down Expand Up @@ -677,3 +693,73 @@ fn detection_query_time_window_is_set() {
assert!(tw.start.as_ref().unwrap().contains('T'));
assert!(tw.end.as_ref().unwrap().contains('T'));
}

#[test]
fn build_technique_detections_sub_technique_parent_t1046() {
// T1046.999 → parent T1046 → build_t1046
let state = SharedRedTeamState::new("test-op".to_string());
let start = Utc::now() - chrono::Duration::hours(1);
let end = Utc::now();
let detections = build_technique_detections(&state, &["T1046.999".to_string()], &start, &end);
let det = &detections["T1046.999"];
assert!(!det.detection_queries.is_empty());
}

#[test]
fn build_technique_detections_sub_technique_parent_t1078() {
// T1078.999 → parent T1078 → build_t1078
let state = SharedRedTeamState::new("test-op".to_string());
let start = Utc::now() - chrono::Duration::hours(1);
let end = Utc::now();
let detections = build_technique_detections(&state, &["T1078.999".to_string()], &start, &end);
let det = &detections["T1078.999"];
assert!(!det.detection_queries.is_empty());
}

#[test]
fn build_technique_detections_sub_technique_parent_t1558() {
// T1558.999 → parent T1558 → build_t1558
let state = SharedRedTeamState::new("test-op".to_string());
let start = Utc::now() - chrono::Duration::hours(1);
let end = Utc::now();
let detections = build_technique_detections(&state, &["T1558.999".to_string()], &start, &end);
let det = &detections["T1558.999"];
assert!(!det.detection_queries.is_empty());
}

#[test]
fn build_technique_detections_sub_technique_parent_t1021() {
// T1021.999 → parent T1021 → build_t1021
let state = SharedRedTeamState::new("test-op".to_string());
let start = Utc::now() - chrono::Duration::hours(1);
let end = Utc::now();
let detections = build_technique_detections(&state, &["T1021.999".to_string()], &start, &end);
let det = &detections["T1021.999"];
assert!(!det.detection_queries.is_empty());
}

#[test]
fn build_technique_detections_sub_technique_parent_t1550() {
// T1550.999 → parent T1550 → build_t1550
let state = SharedRedTeamState::new("test-op".to_string());
let start = Utc::now() - chrono::Duration::hours(1);
let end = Utc::now();
let detections = build_technique_detections(&state, &["T1550.999".to_string()], &start, &end);
let det = &detections["T1550.999"];
assert!(!det.detection_queries.is_empty());
}

#[test]
fn build_technique_detections_unknown_with_known_name() {
// A technique that has a known name (not empty) in get_technique_name
// The T9999 was used in unknown_technique_fallback; use a known one that still
// hits the fallback branch (T1484 is in the names table but not the match arms)
let state = SharedRedTeamState::new("test-op".to_string());
let start = Utc::now() - chrono::Duration::hours(1);
let end = Utc::now();
let detections = build_technique_detections(&state, &["T1484".to_string()], &start, &end);
let det = &detections["T1484"];
// T1484 hits the final fallback; name comes from get_technique_name
assert_eq!(det.technique_id, "T1484");
assert_eq!(det.technique_name, "Domain Policy Modification");
}
Loading
Loading