Detection rules › Kusto
Netskope - Suspicious Network Context (Unusual IPs/Geo/Ports)
Detects suspicious network activity based on unusual source/destination IPs, geographic anomalies, uncommon ports, and high traffic volumes.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Discovery | T1046 Network Service Discovery |
| Command & Control | T1071 Application Layer Protocol |
| Exfiltration | T1048 Exfiltration Over Alternative Protocol |
Rule body kusto
id: 6d989fb0-933e-4ae6-88fa-10e7b51c8897
name: Netskope - Suspicious Network Context (Unusual IPs/Geo/Ports)
description: |
Detects suspicious network activity based on unusual source/destination IPs, geographic anomalies, uncommon ports, and high traffic volumes.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: NetskopeWebTxConnector
dataTypes:
- NetskopeWebTransactions_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
- Exfiltration
- Discovery
relevantTechniques:
- T1071
- T1048
- T1046
query: |
NetskopeWebTransactions_CL
| where TimeGenerated > ago(1h)
| where isnotempty(CsUsername)
| extend
DstPort = coalesce(XCsDstPort, XSrDstPort, CsUriPort),
SrcIP = coalesce(XCsSrcIp, CIp),
DstIP = coalesce(XCsDstIp, XSrDstIp, SIp)
| summarize
EventCount = count(),
TotalBytes = sum(Bytes),
UniqueDstIPs = dcount(DstIP),
DstIPs = make_set(DstIP, 20),
UniqueDstPorts = dcount(DstPort),
DstPorts = make_set(DstPort, 20),
UniqueHosts = dcount(CsHost),
Hosts = make_set(CsHost, 20),
Countries = make_set(XSCountry),
SuspiciousPortHits = countif(DstPort in (20, 21, 22, 23, 25, 445, 1433, 1434, 3306, 3389, 5432, 5900, 5901)),
HighRiskCountryHits = countif(XSCountry in ('RU', 'CN', 'KP', 'IR', 'SY'))
by CsUsername, XCsSrcIp, XCCountry, XCLocation, bin(TimeGenerated, 1h)
| where SuspiciousPortHits > 0 or HighRiskCountryHits > 0 or UniqueDstIPs > 50 or TotalBytes > 1073741824
| extend
TotalMB = round(TotalBytes / 1048576.0, 2),
RiskFactors = strcat_array(array_concat(
iff(SuspiciousPortHits > 0, dynamic(['Suspicious Ports']), dynamic([])),
iff(HighRiskCountryHits > 0, dynamic(['High Risk Country']), dynamic([])),
iff(UniqueDstIPs > 50, dynamic(['Many Destinations']), dynamic([])),
iff(TotalBytes > 1073741824, dynamic(['High Volume']), dynamic([]))
), ', ')
| project
TimeGenerated,
User = CsUsername,
SourceIP = XCsSrcIp,
SourceCountry = XCCountry,
SourceLocation = XCLocation,
DestinationIPs = DstIPs,
UniqueDstIPCount = UniqueDstIPs,
DestinationPorts = DstPorts,
TargetHosts = Hosts,
DestinationCountries = Countries,
SuspiciousPortAccessCount = SuspiciousPortHits,
HighRiskCountryAccessCount = HighRiskCountryHits,
TotalDataMB = TotalMB,
EventCount,
RiskFactors
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: User
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
NetskopeWebTransactions_CL
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: where
| where isnotempty(CsUsername)
Stage 4: extend
| extend
DstPort = coalesce(XCsDstPort, XSrDstPort, CsUriPort),
SrcIP = coalesce(XCsSrcIp, CIp),
DstIP = coalesce(XCsDstIp, XSrDstIp, SIp)
Stage 5: summarize
| summarize
EventCount = count(),
TotalBytes = sum(Bytes),
UniqueDstIPs = dcount(DstIP),
DstIPs = make_set(DstIP, 20),
UniqueDstPorts = dcount(DstPort),
DstPorts = make_set(DstPort, 20),
UniqueHosts = dcount(CsHost),
Hosts = make_set(CsHost, 20),
Countries = make_set(XSCountry),
SuspiciousPortHits = countif(DstPort in (20, 21, 22, 23, 25, 445, 1433, 1434, 3306, 3389, 5432, 5900, 5901)),
HighRiskCountryHits = countif(XSCountry in ('RU', 'CN', 'KP', 'IR', 'SY'))
by CsUsername, XCsSrcIp, XCCountry, XCLocation, bin(TimeGenerated, 1h)
Stage 6: where
| where SuspiciousPortHits > 0 or HighRiskCountryHits > 0 or UniqueDstIPs > 50 or TotalBytes > 1073741824
Stage 7: extend
| extend
TotalMB = round(TotalBytes / 1048576.0, 2),
RiskFactors = strcat_array(array_concat(
iff(SuspiciousPortHits > 0, dynamic(['Suspicious Ports']), dynamic([])),
iff(HighRiskCountryHits > 0, dynamic(['High Risk Country']), dynamic([])),
iff(UniqueDstIPs > 50, dynamic(['Many Destinations']), dynamic([])),
iff(TotalBytes > 1073741824, dynamic(['High Volume']), dynamic([]))
), ', ')
Stage 8: project
| project
TimeGenerated,
User = CsUsername,
SourceIP = XCsSrcIp,
SourceCountry = XCCountry,
SourceLocation = XCLocation,
DestinationIPs = DstIPs,
UniqueDstIPCount = UniqueDstIPs,
DestinationPorts = DstPorts,
TargetHosts = Hosts,
DestinationCountries = Countries,
SuspiciousPortAccessCount = SuspiciousPortHits,
HighRiskCountryAccessCount = HighRiskCountryHits,
TotalDataMB = TotalMB,
EventCount,
RiskFactors
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
CsUsername | is_not_null | |
HighRiskCountryHits | gt |
|
SuspiciousPortHits | gt |
|
TotalBytes | gt |
|
UniqueDstIPs | gt |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
DestinationCountries | project |
DestinationIPs | project |
DestinationPorts | project |
EventCount | project |
HighRiskCountryAccessCount | project |
RiskFactors | project |
SourceCountry | project |
SourceIP | project |
SourceLocation | project |
SuspiciousPortAccessCount | project |
TargetHosts | project |
TimeGenerated | project |
TotalDataMB | project |
UniqueDstIPCount | project |
User | project |