Detection rules › Kusto
GitLab - Brute-force Attempts
'This query relies on GitLab Application Logs to get failed logins to highlight brute-force attempts from different IP addresses in a short space of time.'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule body
id: 2238d13a-cf05-4973-a83f-d12a25dbb153
name: GitLab - Brute-force Attempts
description: |
'This query relies on GitLab Application Logs to get failed logins to highlight brute-force attempts from different IP addresses in a short space of time.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let LearningPeriod = 7d;
let EndLearningTime = now();
let BinTime = 1h;
let RunTime = 1h;
let MinThreshold = 3.0;
let GitLabFailedLogins = (GitLabApp
| where FailedLogin == 1
| parse kind=regex Message with "Failed Login: username=" User "ip=" IpAddress
| project TimeGenerated, EventTime, Computer, User, HostName, HostIP, IpAddress);
GitLabFailedLogins
| where EventTime between (ago(LearningPeriod) .. EndLearningTime)
| summarize FailedLoginsCountInBinTime = count() by User, bin(EventTime, BinTime)
| summarize AvgOfFailedLoginsInLearning = avg(FailedLoginsCountInBinTime), StdOfFailedLoginsInLearning = stdev(FailedLoginsCountInBinTime) by User
| extend LearningThreshold = max_of(AvgOfFailedLoginsInLearning, MinThreshold)
| join kind=innerunique ( GitLabFailedLogins
| summarize FailedLoginsCountInRunTime = count() by User, IpAddress, EventTime = bin(EventTime, BinTime) ) on User
| where FailedLoginsCountInRunTime >= LearningThreshold
| project User, IpAddress, EventTime, FailedLoginsCountInRunTime, LearningThreshold
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let LearningPeriod = 7d;
let EndLearningTime = now();
let BinTime = 1h;
let RunTime = 1h;
let MinThreshold = 3.0;
let GitLabFailedLogins is inlined into the numbered stages below.
Stages 1 to 4 define let GitLabFailedLogins (the rule's main pipeline source); stages 5 to 11 run on it.
Stage 1: source
GitLabApp
Stage 2: where
| where FailedLogin == 1
Stage 3: parse
| parse kind=regex Message with "Failed Login: username=" User "ip=" IpAddress
Stage 4: project
| project TimeGenerated, EventTime, Computer, User, HostName, HostIP, IpAddress
Stage 5: where
GitLabFailedLogins
| where EventTime between (ago(LearningPeriod) .. EndLearningTime)
Stage 6: summarize
| summarize FailedLoginsCountInBinTime = count() by User, bin(EventTime, BinTime)
Stage 7: summarize
| summarize AvgOfFailedLoginsInLearning = avg(FailedLoginsCountInBinTime), StdOfFailedLoginsInLearning = stdev(FailedLoginsCountInBinTime) by User
Stage 8: extend
| extend LearningThreshold = max_of(AvgOfFailedLoginsInLearning, MinThreshold)
Stage 9: join
| join kind=innerunique ( GitLabFailedLogins
| summarize FailedLoginsCountInRunTime = count() by User, IpAddress, EventTime = bin(EventTime, BinTime) ) on User
Stage 10: where
| where FailedLoginsCountInRunTime >= LearningThreshold
Stage 11: project
| project User, IpAddress, EventTime, FailedLoginsCountInRunTime, LearningThreshold
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
FailedLogin | eq |
| field:"FailedLogin" kind:eq value:"1" |
FailedLoginsCountInRunTime | cross_field_compare |
| field:"FailedLoginsCountInRunTime" kind:cross_field_compare value:"LearningThreshold" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EventTime | project |
FailedLoginsCountInRunTime | project |
IpAddress | project |
LearningThreshold | project |
User | project |