File size: 9,703 Bytes
afa0cbf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | #[cfg(any(target_os = "macos", target_os = "windows"))]
use std::ffi::OsStr;
#[cfg(target_os = "windows")]
use std::path::PathBuf;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use std::process::Output;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use std::process::Stdio;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use std::time::Duration;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use tokio::process::Command;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use tokio::time::timeout;
use super::CheckStatus;
use super::DoctorCheck;
use super::DoctorIssue;
#[cfg(any(target_os = "macos", target_os = "windows"))]
const PRODUCT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(any(target_os = "macos", target_os = "windows"))]
const MAX_PRODUCT_OUTPUT_BYTES: usize = 64 * 1024;
pub(super) enum EndpointInspection {
Complete(Vec<&'static str>),
#[cfg(any(target_os = "windows", test))]
Partial(Vec<&'static str>),
#[cfg(any(target_os = "macos", target_os = "windows", test))]
Unavailable,
}
pub(super) async fn check() -> DoctorCheck {
endpoint_check(endpoint_products().await)
}
pub(super) fn endpoint_check(inspection: EndpointInspection) -> DoctorCheck {
let (products, visibility_incomplete) = match inspection {
EndpointInspection::Complete(products) => (products, false),
#[cfg(any(target_os = "windows", test))]
EndpointInspection::Partial(products) => (products, true),
#[cfg(any(target_os = "macos", target_os = "windows", test))]
EndpointInspection::Unavailable => {
return DoctorCheck::new(
"security.endpoint",
"security",
CheckStatus::Warning,
"endpoint protection inspection unavailable",
)
.detail("endpoint products: unavailable");
}
};
if products.is_empty() {
let (summary, detail) = if cfg!(any(target_os = "macos", target_os = "windows")) {
("no supported endpoint protection detected", "none detected")
} else {
(
"endpoint protection is not inspected on this platform",
"not inspected on this platform",
)
};
return DoctorCheck::new("security.endpoint", "security", CheckStatus::Ok, summary)
.detail(format!("endpoint products: {detail}"));
}
let targets = if cfg!(target_os = "windows") {
"signed Codex app; codex.exe; codex-windows-sandbox-setup.exe; codex-command-runner.exe; codex-code-mode-host.exe"
} else if cfg!(target_os = "macos") {
"signing team 2DC432GLL2 plus the installed Codex app identity, signed codex agent, and required helpers"
} else {
"verified Codex app, codex agent, and required helpers"
};
let mut check = DoctorCheck::new(
"security.endpoint",
"security",
CheckStatus::Warning,
"endpoint protection detected; Codex exclusions are unverified",
)
.detail(format!("endpoint products: {}", products.join(", ")))
.detail(format!("exclusion targets: {targets}"))
.detail("Codex exclusions: not verified")
.remediation(
"ask your security administrator to verify Codex exclusions and required helper allowances",
);
if visibility_incomplete {
check = check.detail("additional endpoint products: unavailable");
}
for product in products {
let remedy = match product {
"CrowdStrike Falcon" => {
"CrowdStrike Falcon: Add a certificate or IOA exclusion for Codex. If sensor overhead continues, exclude the Codex agent from sensor visibility. Keep monitoring descendant processes."
}
"BeyondTrust Privilege Management" => {
"BeyondTrust: Remove Codex from application blocking rules. Add allow rules for Codex helper executables. Do not grant administrator privileges."
}
"Microsoft Defender" => {
"Microsoft Defender: Add a certificate or executable-path exclusion for Codex and its helpers. If Attack Surface Reduction blocks Codex, add a rule exclusion. If Controlled Folder Access blocks Codex, allow the app."
}
"SentinelOne" => {
"SentinelOne: Add a signer, file-hash, or executable-path exclusion for Codex and its helpers."
}
"Jamf Protect" => {
"Jamf Protect: Add an Override Threat Prevention exception for Codex app and helper signing identities. If analytics cause delays, add an Ignore System Events for Analytics exception."
}
_ => {
"Add an exclusion for Codex and its helpers. Use the endpoint product instructions."
}
};
check = check.issue(
DoctorIssue::new(
CheckStatus::Warning,
format!("{product} can interfere with Codex. Verify Codex exclusions."),
)
.measured("not verified")
.expected("Codex application and helper exclusions")
.remedy(remedy)
.field("Codex exclusions"),
);
}
check
}
async fn endpoint_products() -> EndpointInspection {
#[cfg(target_os = "windows")]
{
let system32 = std::env::var_os("SystemRoot")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(r"C:\Windows"))
.join("System32");
let service = system32.join("sc.exe");
let (crowdstrike, beyondtrust, defender, sentinelone) = tokio::join!(
product_command(&service, &["query", "CSFalconService"]),
product_command(&service, &["query", "DefendpointService"]),
product_command(&service, &["query", "WinDefend"]),
product_command(&service, &["query", "SentinelAgent"]),
);
let mut products = Vec::new();
let mut visibility_incomplete = false;
for (product, output) in [
("CrowdStrike Falcon", crowdstrike),
("BeyondTrust Privilege Management", beyondtrust),
("Microsoft Defender", defender),
("SentinelOne", sentinelone),
] {
match output {
Some(output) if output.status.success() => {
if product != "Microsoft Defender"
|| String::from_utf8_lossy(&output.stdout).lines().any(|line| {
line.split_once(':').is_some_and(|(_, value)| {
value.split_whitespace().next() == Some("4")
})
})
{
products.push(product);
}
}
Some(output) if output.status.code() == Some(1060) => {}
Some(_) | None => visibility_incomplete = true,
}
}
if visibility_incomplete {
if products.is_empty() {
EndpointInspection::Unavailable
} else {
EndpointInspection::Partial(products)
}
} else {
EndpointInspection::Complete(products)
}
}
#[cfg(target_os = "macos")]
{
const PRODUCTS: &[(&str, &str, &str)] = &[
(
"CrowdStrike Falcon",
"X9E956P446",
"com.crowdstrike.falcon.Agent",
),
(
"BeyondTrust Privilege Management",
"2ZS8T6NYB8",
"com.beyondtrust.endpointsecurity",
),
(
"Microsoft Defender",
"UBF8T346G9",
"com.microsoft.wdav.epsext",
),
(
"SentinelOne",
"4AYE5J54KN",
"com.sentinelone.network-monitoring",
),
(
"Jamf Protect",
"483DWKW443",
"com.jamf.protect.security-extension",
),
];
let Some(output) = product_command("/usr/bin/systemextensionsctl", &["list"])
.await
.filter(|output| output.status.success())
else {
return EndpointInspection::Unavailable;
};
EndpointInspection::Complete(
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| {
let mut columns = line.split_whitespace();
if columns.next() != Some("*") || columns.next() != Some("*") {
return None;
}
let team = columns.next()?;
let bundle = columns.next()?;
PRODUCTS.iter().find_map(|&(name, signer, identifier)| {
(team == signer && bundle == identifier).then_some(name)
})
})
.collect(),
)
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
EndpointInspection::Complete(Vec::new())
}
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
async fn product_command(program: impl AsRef<OsStr>, args: &[&str]) -> Option<Output> {
let mut command = Command::new(program);
command
.args(args)
.stdin(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true);
timeout(PRODUCT_QUERY_TIMEOUT, command.output())
.await
.ok()?
.ok()
.filter(|output| output.stdout.len() <= MAX_PRODUCT_OUTPUT_BYTES)
}
#[cfg(test)]
#[path = "security_tests.rs"]
mod tests;
|