Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ SkillGroups:
| extend TargetResourcesJson = parse_json(tostring(TargetResources))
| mv-expand TargetResource = TargetResourcesJson
| extend userPrincipalName = tostring(TargetResource.userPrincipalName)
| where userPrincipalName == upn
| where userPrincipalName =~ upn
| summarize PasswordChangeCount = count() by bin(TimeGenerated, 1d), userPrincipalName
| project TimeGenerated, PasswordChangeCount, userPrincipalName
| order by TimeGenerated desc
Expand Down Expand Up @@ -334,26 +334,37 @@ SkillGroups:
Description: Identify failed sign-in attempts for a specific user over a defined lookback period. Provides detailed insights into failed attempts, including timestamps, IP addresses, locations, and result descriptions, to detect potential unauthorized access or sign-in issues.
Inputs:
- Name: upn
Description: User principal name. i.e., [email protected]
Description: User principal name (e.g., [email protected])
Required: true
- Name: date
Copy link
Preview

Copilot AI Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'date' parameter lacks input validation in the KQL template. Consider adding validation to ensure the date format matches YYYY-MM-DD and handle invalid date inputs gracefully to prevent query errors.

Copilot uses AI. Check for mistakes.

Description: Calendar date to filter on (YYYY-MM-DD, UTC)
Required: true
- Name: lookback_period
Description: The time range to look back. i.e., 1d, 7d, 30d (default is 1d)
Required: false
- Name: exclude_ip
Description: Any IP address that should be excluded from the results.
Description: Any IP address that should be excluded from the results
Default: ""
Required: false
- Name: user_context
Description: Additional context for tuning the output or investigation
Default: ""
Required: false
Settings:
Target: Defender
Template: |-
// Query failed sign-in attempts for a specific user on a specific UTC date.
// user_context: {{user_context}}
let startDate = datetime("{{date}}"); // Parse the input date (YYYY-MM-DD, UTC)
Copy link
Preview

Copilot AI Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct string interpolation of user input into datetime() function without validation could lead to injection issues or query failures. Consider validating the date format before using it in the KQL query.

Suggested change
let startDate = datetime("{{date}}"); // Parse the input date (YYYY-MM-DD, UTC)
let isValidDate = tostring("{{date}}") matches regex @"^\d{4}-\d{2}-\d{2}$"; // Validate date format (YYYY-MM-DD)
let startDate = isValidDate ? datetime("{{date}}") : datetime(null); // Parse the input date or set to null if invalid
let endDate = startDate + 1d; // Compute the end of the day window (exclusive)

Copilot uses AI. Check for mistakes.

let endDate = startDate + 1d; // Compute the end of the day window (exclusive)
SigninLogs
| where TimeGenerated >= ago(1d) // Specify the lookback_period, default 1d
| where IPAddress != "{{exclude_ip}}" // exclude_ip from query
| where ResultType != 0 // Filtering for failed sign-ins; 0 indicates success
| where UserPrincipalName == "{{upn}}"
| summarize FailedAttempts = count(), UniqueIPs = dcount(IPAddress), Locations = dcount(Location), ResultDescriptions = makeset(ResultDescription) by bin(TimeGenerated, 1d)
| project TimeGenerated, FailedAttempts, UniqueIPs, Locations, ResultDescriptions
| order by TimeGenerated desc
| where TimeGenerated >= startDate and TimeGenerated < endDate // Only events within that calendar day (UTC)
| where UserPrincipalName =~ "{{upn}}" // Case-insensitive match on UPN
| where IPAddress != "{{exclude_ip}}" // Optionally exclude a specific IP address
| where ResultType != 0 // Only failed attempts (0 = success)
// Summarize for SOC: count, unique IPs, unique locations, reasons
| summarize
FailedAttempts = count(), // Total failed attempts for the user that day
UniqueIPs = dcount(IPAddress), // Number of distinct IP addresses used
Locations = dcount(Location), // Number of unique country codes/locations
ResultDescriptions = makeset(ResultDescription) // All unique failure reasons seen

- Name: EnhancedUserRiskAssessment
DisplayName: Enhanced User Risk Assessment
Expand All @@ -373,7 +384,7 @@ SkillGroups:
let TimeFrame = ago(7d);
let upn = "{{upn}}";
let UserRiskEvents = AADUserRiskEvents
| where TimeGenerated > TimeFrame and RiskLevel != "none" and RiskState != "remediated" and UserPrincipalName == upn
| where TimeGenerated > TimeFrame and RiskLevel != "none" and RiskState != "remediated" and UserPrincipalName =~ upn
| project
UserPrincipalName,
RiskLevel,
Expand Down Expand Up @@ -527,7 +538,7 @@ SkillGroups:
| extend TimeDifferenceHours = datetime_diff('second', TimeGenerated, PreviousTime) / 3600.0
| extend VelocityMph = DistanceMiles / TimeDifferenceHours
| where VelocityMph > MaxVelocityMph
| where UserPrincipalName == "{{upn}}"
| where UserPrincipalName =~ "{{upn}}"
| project UserPrincipalName, TimeGenerated, PreviousTime, IPAddress, PreviousIPAddress, DistanceMiles, TimeDifferenceHours, VelocityMph, Country, PreviousCountry

- Name: SuspiciousMailboxActivities
Expand Down Expand Up @@ -657,3 +668,58 @@ SkillGroups:
| where IPAddress == TargetIP // Filter events matching the specified IP
| where ResultType == 0 // Keep only successful sign-in events
| summarize SuccessCount = count() // Return the number of successful sign-ins

- Name: UserSigninActivityLast48h
DisplayName: User Sign-in Activity (Last 48 Hours)
DescriptionForModel: |-
Performs a KQL query on the `SigninLogs` table to retrieve all sign-in attempts (both successful and failed) for a specified user over the past 48 hours, using the current schema. Features:
- Provides a timeline of all sign-in activity for the user, including both interactive and non-interactive sign-ins.
- Includes ResultType and ResultDescription for error code analysis, helping SOC analysts rapidly triage common issues such as account lockouts, expired passwords, missing app assignments, and MFA/Conditional Access failures.
- Captures contextual details (IP, location, device, user agent, application) to support correlation and investigation.
- Facilitates rapid detection of brute force attempts, misconfiguration, new/unusual failure types, and compliance with MFA and Conditional Access policies.
Description: Retrieve all sign-in attempts—successful and failed—for a specific user in the past 48 hours, with key details and error context for SOC investigation.
Inputs:
- Name: upn
Description: User principal name (e.g., [email protected])
Required: true
- Name: exclude_ip
Description: Any IP address to exclude from results
Default: ""
Required: false
- Name: signin_context
Description: Additional context or investigation details
Default: ""
Required: false
Settings:
Target: Defender
Template: |-
// User sign-in activity for the last 48 hours, both successful and failed.
// Additional context: {{signin_context}}
let UPN = "{{upn}}";
let TimeFrame = ago(48h);
SigninLogs
| where TimeGenerated > TimeFrame
| where UserPrincipalName =~ UPN
| where IPAddress != "{{exclude_ip}}"
// Project key fields for timeline and investigation
| project
TimeGenerated, // When the sign-in occurred
UserPrincipalName, // The user account (UPN)
UserDisplayName, // User's display name
UserId, // User GUID
ResultType, // Numeric result code (0 = success; others = failure)
ResultDescription, // Human-readable description of failure/success
Status, // Additional status info (JSON: errorCode, failureReason, details)
// Common codes: 0 (success), 50053 (locked), 50055 (password expired), 50126 (bad credentials),
// 50074/50076/50079 (MFA/CA issues), 50105 (not assigned), 700016 (app not found), 70008 (token expired)
IPAddress, // Source IP of the sign-in
Location, // Country code (two-letter)
LocationDetails, // City, state, geo coordinates (dynamic)
DeviceDetail, // Device info (dynamic: deviceId, OS, browser)
UserAgent, // Raw user agent string
AppDisplayName, // Application/service name (e.g., Outlook, Graph Explorer)
AppId, // Application's Azure AD AppId
ClientAppUsed, // Legacy client protocol type (e.g., Browser, Modern client)
SessionId, // Session GUID (useful for tracking multi-stage auth)
CorrelationId // Correlate multiple related sign-in events
| order by TimeGenerated desc