diff --git a/.github/workflows/content-validation.yml b/.github/workflows/content-validation.yml new file mode 100644 index 0000000..d00deaf --- /dev/null +++ b/.github/workflows/content-validation.yml @@ -0,0 +1,20 @@ +name: Analytics rule validation + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: {python-version: "3.12"} + - run: python -m pip install --disable-pip-version-check -r requirements-dev.txt + - run: python scripts/validate_rules.py + - run: python scripts/generate_catalog.py --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..124727f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.DS_Store +.venv/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2de34d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased — 2.0.0 + +- Restructure the repository around root-level analytics rules. +- Rebuild four starter rules with unique GUIDs, bounded queries, mappings, alert enrichment, and incident settings. +- Add cloud, endpoint, and UEBA rule domains. +- Add automated validation, generated catalog, schemas, fixtures, architecture, and deployment guidance. +- Remove unrelated portfolio bootstrap, profile, workbook, and repository-template artifacts. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bac9923 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing and Maintainer Workflow + +The repository is not currently accepting unsolicited contributions. This guide documents the maintainer's quality standard. + +Every rule must include a stable GUID, semantic version, required connectors, schedule, severity, MITRE mapping where applicable, `TimeGenerated` in the final result, mapped entities, visible custom details, alert grouping, incident configuration, false-positive guidance, and response guidance. + +## Workflow + +1. Develop and test the KQL in a non-production Sentinel workspace. +2. Exercise positive, negative, and boundary cases. +3. Add or update a self-contained fixture when practical. +4. Run `python3 scripts/validate_rules.py`. +5. Regenerate the catalog with `python3 scripts/generate_catalog.py`. +6. Record schema assumptions and test evidence in the pull request. + +Increment the rule version when logic, tables, thresholds, mappings, schedule, or incident behavior changes. Never commit credentials, tenant identifiers, personal data, real incident evidence, or unredacted prompts and responses. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..75dcd33 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Leandro Rocha + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 79e8382..8ae43cd 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,64 @@ -# GitHub Portfolio Security Engineering Bundle +# Microsoft Sentinel Analytics Rules -This bundle contains artifacts to improve and professionalize a cybersecurity GitHub portfolio focused on: +A focused detection-as-code library for Microsoft Sentinel covering identity, endpoint, cloud, Microsoft Purview, AI security, Microsoft Agent 365, and UEBA. -- Detection Engineering -- Incident Response -- Microsoft Sentinel -- Microsoft Defender XDR -- Microsoft Purview -- Microsoft Agent 365 -- AI Security -- SOAR / Logic Apps -- KQL -- Snort -- Sigma +Every rule is an adaptable example. Validate connector identifiers, table schemas, thresholds, entity mappings, grouping, and expected alert volume in a non-production workspace before enablement. -## Included Artifacts +## Architecture -```text -. -├── profile-readme/ -│ └── leandroer/ -│ └── README.md -├── repo-templates/ -│ ├── STANDARD-REPO-README.md -│ ├── DETECTION-DOCUMENTATION-TEMPLATE.md -│ └── PROJECT-ROADMAP.md -├── diagrams/ -│ ├── ai-security-operations-architecture.md -│ ├── sentinel-soar-architecture.md -│ └── snort-detection-architecture.md -├── Sentinel-Analytics-Rules/ -├── Sentinel-Workbooks/ -└── deployment/ - └── DEPLOYMENT-COMMANDS.md +```mermaid +flowchart LR + T["Security telemetry"] --> K["KQL detection"] + K --> E["Entity and context enrichment"] + E --> A["Sentinel alert"] + A --> I["Incident grouping"] + I --> R["Investigation and response"] + R --> M["Precision and coverage measurement"] + M --> K ``` -## Recommended GitHub Profile Pins - -Pin these repositories in this order: +## Rule domains -1. `IncidentResponse` -2. `AI-Security-Incident-Response-Lab` -3. `KQL-Templates` -4. `Snort-Detection-Engineering-Lab` -5. `Sentinel-Analytics-Rules` -6. `Sigma-Templates` +| Domain | Example coverage | +|---|---| +| AI security | Repeated prompt-injection indicators | +| Agent 365 | Risky or unauthorized agent tool use | +| Identity | Password spraying | +| Purview | AI activity correlated with mass file operations | +| Cloud | Azure resource deletion | +| Endpoint | Suspicious PowerShell execution | +| UEBA | Privileged-identity anomaly | -## Recommended GitHub Topics +Browse the generated [rule catalog](docs/rule-catalog.md) for tables, severity, ATT&CK mappings, entities, and versions. -Use lowercase topics only: +## Repository structure ```text -cybersecurity -incident-response -detection-engineering -microsoft-sentinel -defender-xdr -microsoft-purview -ai-security -kql -soc -threat-hunting -logic-apps -snort -sigma - -## Repository Notice - -This repository is maintained as a professional cybersecurity portfolio and research project focused on detection engineering, incident response, threat hunting, security automation, and AI security. -Content is published for educational, technical, and professional reference purposes. This repository is not currently accepting community contributions, pull requests, issue submissions, or feature requests. -All content is provided as-is and should be reviewed, tested, and validated before use in production environments. -blue-team +analytics-rules/ Scheduled rule YAML organized by domain +docs/ Authoring, architecture, schema, and validation guidance +scripts/ Static validation and catalog generation +tests/fixtures/ Self-contained KQL logic fixtures +.github/workflows/ Pull-request validation +``` + +## Validate + +```bash +python3 -m pip install -r requirements-dev.txt +python3 scripts/validate_rules.py +python3 scripts/generate_catalog.py --check ``` + +Static validation does not prove that a rule compiles against tenant data. See [validation strategy](docs/validation-strategy.md) for workspace-backed testing requirements. + +See the [coverage matrix](docs/coverage-matrix.md) for current behavioral and telemetry coverage, and [deployment guidance](deployment/README.md) before attempting tenant deployment. + +## Validation labels + +- **Example** — structurally reviewed, but not executed against representative telemetry. +- **Lab Tested** — executed with controlled positive and negative scenarios. +- **Production Validated** — operated against representative production telemetry with documented results. + +## Notice + +This repository is a professional cybersecurity portfolio and defensive research project. Unsolicited community contributions, issue submissions, and feature requests are not currently accepted. Content is provided as-is and must be reviewed, tested, authorized, and tuned before production deployment. diff --git a/Sentinel-Analytics-Rules/.github/ISSUE_TEMPLATE/analytics-rule-request.md b/Sentinel-Analytics-Rules/.github/ISSUE_TEMPLATE/analytics-rule-request.md deleted file mode 100644 index c6cf75a..0000000 --- a/Sentinel-Analytics-Rules/.github/ISSUE_TEMPLATE/analytics-rule-request.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Analytics Rule Request -about: Request a new Sentinel analytics rule -title: "[Rule]: " -labels: analytics-rule, sentinel -assignees: '' ---- - -## Detection Objective - -## Required Data Sources - -## Proposed Logic - -## False Positives - -## Response Guidance diff --git a/Sentinel-Analytics-Rules/README.md b/Sentinel-Analytics-Rules/README.md deleted file mode 100644 index b29451f..0000000 --- a/Sentinel-Analytics-Rules/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Sentinel Analytics Rules - -Professional Microsoft Sentinel analytics rule library for Identity, Microsoft Purview, AI Security, Microsoft Agent 365, and SOC operations. - -## Purpose - -This repository provides production-minded Sentinel analytics rule examples with: - -- KQL detection logic -- YAML rule structure -- MITRE ATT&CK mapping -- required data connectors -- false-positive guidance -- tuning guidance -- response recommendations - -## Repository Structure - -```text -. -├── analytics/ -│ ├── Identity/ -│ ├── Purview/ -│ ├── AI-Security/ -│ └── Agent-365/ -└── docs/ -``` - -## Rule Quality Standard - -Each rule should include: - -- clear rule name -- severity -- query frequency -- query period -- MITRE tactic and technique -- data connector requirements -- entity mappings -- incident response guidance - -## Disclaimer - -Validate and tune all rules before production deployment. diff --git a/Sentinel-Analytics-Rules/analytics/AI-Security/Prompt-Injection-Attempt.yaml b/Sentinel-Analytics-Rules/analytics/AI-Security/Prompt-Injection-Attempt.yaml deleted file mode 100644 index 7721ffe..0000000 --- a/Sentinel-Analytics-Rules/analytics/AI-Security/Prompt-Injection-Attempt.yaml +++ /dev/null @@ -1,37 +0,0 @@ -id: 33333333-3333-3333-3333-333333333333 -name: AI Prompt Injection Attempt -description: | - Detects common prompt injection and jailbreak terms in AI application prompts. Tune table and column names to match your AI logging schema. -severity: High -status: Available -requiredDataConnectors: [] -queryFrequency: 1h -queryPeriod: 24h -triggerOperator: gt -triggerThreshold: 0 -tactics: - - InitialAccess -relevantTechniques: - - T1190 -query: | - let suspicious_terms = dynamic([ - "ignore previous instructions", - "ignore all prior instructions", - "reveal your system prompt", - "show me your hidden instructions", - "jailbreak", - "bypass safety", - "developer message" - ]); - AIApp_CL - | where TimeGenerated > ago(24h) - | where Prompt_s has_any (suspicious_terms) - | extend DetectionName = "AI Prompt Injection Attempt" - | extend Severity = "High" -entityMappings: - - entityType: Account - fieldMappings: - - identifier: FullName - columnName: UserPrincipalName_s -version: 1.0.0 -kind: Scheduled diff --git a/Sentinel-Analytics-Rules/analytics/Agent-365/Unauthorized-Agent-Tool-Use.yaml b/Sentinel-Analytics-Rules/analytics/Agent-365/Unauthorized-Agent-Tool-Use.yaml deleted file mode 100644 index fa1bc30..0000000 --- a/Sentinel-Analytics-Rules/analytics/Agent-365/Unauthorized-Agent-Tool-Use.yaml +++ /dev/null @@ -1,28 +0,0 @@ -id: 44444444-4444-4444-4444-444444444444 -name: Unauthorized Agent Tool Use -description: | - Detects Microsoft Agent 365 or agentic workload tool calls outside approved policy. Tune table names to match your telemetry pipeline. -severity: High -status: Available -requiredDataConnectors: [] -queryFrequency: 1h -queryPeriod: 24h -triggerOperator: gt -triggerThreshold: 0 -tactics: - - Execution -relevantTechniques: - - T1059 -query: | - AIToolExecution_CL - | where TimeGenerated > ago(24h) - | where ApprovedTool_b == false or ToolRisk_s in~ ("High", "Critical") - | extend DetectionName = "Unauthorized Agent Tool Use" - | extend Severity = "High" -entityMappings: - - entityType: Account - fieldMappings: - - identifier: FullName - columnName: UserPrincipalName_s -version: 1.0.0 -kind: Scheduled diff --git a/Sentinel-Analytics-Rules/analytics/Identity/Password-Spray-Indicator.yaml b/Sentinel-Analytics-Rules/analytics/Identity/Password-Spray-Indicator.yaml deleted file mode 100644 index 1ec6d68..0000000 --- a/Sentinel-Analytics-Rules/analytics/Identity/Password-Spray-Indicator.yaml +++ /dev/null @@ -1,35 +0,0 @@ -id: 11111111-1111-1111-1111-111111111111 -name: Password Spray Indicator - Multiple Users Failed From Same IP -description: | - Detects a source IP address generating failed sign-ins against multiple users within a short time window. -severity: High -status: Available -requiredDataConnectors: - - connectorId: AzureActiveDirectory - dataTypes: - - SigninLogs -queryFrequency: 1h -queryPeriod: 24h -triggerOperator: gt -triggerThreshold: 0 -tactics: - - CredentialAccess -relevantTechniques: - - T1110.003 -query: | - let Lookback = 24h; - let UserThreshold = 15; - SigninLogs - | where TimeGenerated > ago(Lookback) - | where ResultType != 0 - | summarize FailedUsers=dcount(UserPrincipalName), Users=make_set(UserPrincipalName, 100), Apps=make_set(AppDisplayName, 50) - by IPAddress, bin(TimeGenerated, 30m) - | where FailedUsers >= UserThreshold - | extend DetectionName = "Password Spray Indicator" -entityMappings: - - entityType: IP - fieldMappings: - - identifier: Address - columnName: IPAddress -version: 1.0.0 -kind: Scheduled diff --git a/Sentinel-Analytics-Rules/analytics/Purview/AI-Assisted-Data-Exfiltration.yaml b/Sentinel-Analytics-Rules/analytics/Purview/AI-Assisted-Data-Exfiltration.yaml deleted file mode 100644 index 26e5c2c..0000000 --- a/Sentinel-Analytics-Rules/analytics/Purview/AI-Assisted-Data-Exfiltration.yaml +++ /dev/null @@ -1,38 +0,0 @@ -id: 22222222-2222-2222-2222-222222222222 -name: AI Assisted Data Exfiltration Correlation -description: | - Correlates AI activity with mass file access or external sharing activity. Tune table names to match your AI telemetry schema. -severity: Critical -status: Available -requiredDataConnectors: - - connectorId: Office365 - dataTypes: - - OfficeActivity -queryFrequency: 1h -queryPeriod: 24h -triggerOperator: gt -triggerThreshold: 0 -tactics: - - Exfiltration -relevantTechniques: - - T1041 -query: | - let AIUsers = - AIApp_CL - | where TimeGenerated > ago(24h) - | summarize AIPromptCount=count(), AISessions=make_set(SessionId_s, 50) by UserPrincipalName_s; - OfficeActivity - | where TimeGenerated > ago(24h) - | where Operation in ("FileAccessed", "FileDownloaded", "FileSyncDownloadedFull", "AnonymousLinkCreated", "SharingSet") - | summarize FileEvents=count(), Operations=make_set(Operation, 50), Sites=make_set(Site_Url, 25) - by UserPrincipalName_s = UserId, bin(TimeGenerated, 15m) - | join kind=inner AIUsers on UserPrincipalName_s - | where FileEvents > 100 - | extend DetectionName = "AI Assisted Data Exfiltration Correlation" -entityMappings: - - entityType: Account - fieldMappings: - - identifier: FullName - columnName: UserPrincipalName_s -version: 1.0.0 -kind: Scheduled diff --git a/Sentinel-Analytics-Rules/docs/rule-authoring-guide.md b/Sentinel-Analytics-Rules/docs/rule-authoring-guide.md deleted file mode 100644 index b4cf409..0000000 --- a/Sentinel-Analytics-Rules/docs/rule-authoring-guide.md +++ /dev/null @@ -1,22 +0,0 @@ -# Sentinel Analytics Rule Authoring Guide - -## Recommended Rule Development Lifecycle - -1. Write hunting query. -2. Test against 7-30 days of data. -3. Identify false positives. -4. Add thresholds and allowlists. -5. Add entity mappings. -6. Add MITRE ATT&CK mapping. -7. Add response guidance. -8. Deploy as scheduled analytics rule. -9. Review alert volume after deployment. - -## KQL Best Practices - -- Filter by time early. -- Use `project` before joins. -- Use `make_set(Column, 50)` with explicit max size. -- Use `let` statements for readability. -- Avoid overly broad matching. -- Build thresholds from baseline behavior. diff --git a/Sentinel-Workbooks/README.md b/Sentinel-Workbooks/README.md deleted file mode 100644 index ceed4c1..0000000 --- a/Sentinel-Workbooks/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Sentinel Workbooks - -Starter repository for Microsoft Sentinel workbook concepts aligned to Incident Response, AI Security, Microsoft Purview, and SOC operations. - -## Planned Workbooks - -- AI Security Operations Workbook -- Microsoft Purview Data Exfiltration Workbook -- Incident Response Operations Workbook -- SOC Analytics Rule Health Workbook -- Microsoft Agent 365 Security Workbook - -## Recommended Dashboard Sections - -- Incident trends -- Severity distribution -- Top users -- Top source IPs -- AI prompt injection attempts -- Sensitive data exposure events -- Agent tool usage -- Purview DLP events -- Logic App playbook execution status diff --git a/Sentinel-Workbooks/docs/workbook-design-guide.md b/Sentinel-Workbooks/docs/workbook-design-guide.md deleted file mode 100644 index 4cabd51..0000000 --- a/Sentinel-Workbooks/docs/workbook-design-guide.md +++ /dev/null @@ -1,23 +0,0 @@ -# Workbook Design Guide - -## Workbook Goals - -A good Sentinel workbook should answer operational questions quickly: - -- What changed? -- Who is impacted? -- Which detections are noisy? -- Which incidents are critical? -- Which users or agents are risky? -- Which playbooks failed? - -## Recommended Sections - -1. Executive summary -2. Incident volume -3. Detection category -4. Entity view -5. Data security view -6. AI security view -7. Automation health -8. Drilldown queries diff --git a/analytics-rules/agent-365/unauthorized-agent-tool-use.yaml b/analytics-rules/agent-365/unauthorized-agent-tool-use.yaml new file mode 100644 index 0000000..1a00947 --- /dev/null +++ b/analytics-rules/agent-365/unauthorized-agent-tool-use.yaml @@ -0,0 +1,51 @@ +id: 33963328-d448-4465-a9ab-9f055cec97b9 +name: Risky or unauthorized AI agent tool execution +description: Detects repeated denied, unapproved, or high-risk agent tool calls. Adapt fields to the deployed Agent 365 or custom agent telemetry schema. +severity: High +status: Available +requiredDataConnectors: + - connectorId: CustomLogsAma + dataTypes: [AIToolExecution_CL] +queryFrequency: 15m +queryPeriod: 30m +triggerOperator: gt +triggerThreshold: 0 +tactics: [Execution, PrivilegeEscalation] +relevantTechniques: [T1059] +query: | + let AttemptThreshold = 3; + AIToolExecution_CL + | where TimeGenerated > ago(30m) + | extend UserPrincipalName=tostring(UserPrincipalName_s), SourceIP=tostring(SourceIP_s), + AgentId=tostring(AgentId_s), ToolName=tostring(ToolName_s), Result=tostring(Result_s), + RiskLevel=tostring(RiskLevel_s), ApprovalStatus=tostring(ApprovalStatus_s) + | where Result =~ "Denied" or RiskLevel in~ ("High", "Critical") or ApprovalStatus =~ "Missing" + | summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), AttemptCount=count(), + Tools=make_set(ToolName, 25), Results=make_set(Result, 10) by UserPrincipalName, SourceIP, AgentId + | where AttemptCount >= AttemptThreshold + | extend TimeGenerated=EndTime + | project TimeGenerated, StartTime, EndTime, UserPrincipalName, SourceIP, AgentId, AttemptCount, Tools, Results +entityMappings: + - entityType: Account + fieldMappings: [{identifier: FullName, columnName: UserPrincipalName}] + - entityType: IP + fieldMappings: [{identifier: Address, columnName: SourceIP}] +customDetails: {AgentId: AgentId, AttemptCount: AttemptCount, Tools: Tools} +alertDetailsOverride: + alertDisplayNameFormat: "Risky tool use by agent {{AgentId}}" + alertDescriptionFormat: "{{AttemptCount}} risky or denied tool calls were associated with {{UserPrincipalName}}." +eventGroupingSettings: {aggregationKind: AlertPerResult} +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: Selected + groupByEntities: [Account] + groupByAlertDetails: [] + groupByCustomDetails: [] +suppressionDuration: 5h +suppressionEnabled: false +version: 2.0.0 +kind: Scheduled diff --git a/analytics-rules/ai-security/prompt-injection.yaml b/analytics-rules/ai-security/prompt-injection.yaml new file mode 100644 index 0000000..ee95637 --- /dev/null +++ b/analytics-rules/ai-security/prompt-injection.yaml @@ -0,0 +1,50 @@ +id: 19961853-cf81-4622-a921-f0a67d340d65 +name: Repeated prompt-injection indicators +description: Detects repeated prompt-injection phrases in a custom AI activity table. Adapt the schema and map the behavior to OWASP LLM01; no direct Enterprise ATT&CK technique is asserted. +severity: High +status: Available +requiredDataConnectors: + - connectorId: CustomLogsAma + dataTypes: [AIApp_CL] +queryFrequency: 15m +queryPeriod: 30m +triggerOperator: gt +triggerThreshold: 0 +tactics: [] +relevantTechniques: [] +query: | + let AttemptThreshold = 3; + let SuspiciousTerms = dynamic(["ignore previous instructions", "reveal your system prompt", "jailbreak", "bypass safety", "hidden instructions"]); + AIApp_CL + | where TimeGenerated > ago(30m) + | where Prompt_s has_any (SuspiciousTerms) + | summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), AttemptCount=count(), + PromptSamples=make_set(Prompt_s, 10), Sessions=make_set(SessionId_s, 20) + by UserPrincipalName=UserPrincipalName_s, SourceIP=SourceIP_s, Application=Application_s + | where AttemptCount >= AttemptThreshold + | extend TimeGenerated=EndTime + | project TimeGenerated, StartTime, EndTime, UserPrincipalName, SourceIP, Application, AttemptCount, PromptSamples, Sessions +entityMappings: + - entityType: Account + fieldMappings: [{identifier: FullName, columnName: UserPrincipalName}] + - entityType: IP + fieldMappings: [{identifier: Address, columnName: SourceIP}] +customDetails: {Application: Application, AttemptCount: AttemptCount, StartTime: StartTime} +alertDetailsOverride: + alertDisplayNameFormat: "Repeated prompt-injection indicators by {{UserPrincipalName}}" + alertDescriptionFormat: "{{AttemptCount}} suspicious prompts were observed in {{Application}}." +eventGroupingSettings: {aggregationKind: AlertPerResult} +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: Selected + groupByEntities: [Account] + groupByAlertDetails: [] + groupByCustomDetails: [] +suppressionDuration: 5h +suppressionEnabled: false +version: 2.0.0 +kind: Scheduled diff --git a/analytics-rules/cloud/azure-resource-deletion.yaml b/analytics-rules/cloud/azure-resource-deletion.yaml new file mode 100644 index 0000000..fc46ee2 --- /dev/null +++ b/analytics-rules/cloud/azure-resource-deletion.yaml @@ -0,0 +1,47 @@ +id: f945b291-984d-4c02-b33d-77209ba96e94 +name: Successful Azure resource deletion +description: Detects successful Azure control-plane delete operations. Tune for approved automation, ephemeral resources, and change windows. +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: AzureActivity + dataTypes: [AzureActivity] +queryFrequency: 15m +queryPeriod: 30m +triggerOperator: gt +triggerThreshold: 0 +tactics: [Impact] +relevantTechniques: [T1485] +query: | + AzureActivity + | where TimeGenerated > ago(30m) + | where OperationNameValue endswith "/delete" and ActivityStatusValue =~ "Success" + | extend CallerUPN=tostring(Caller), SourceIP=tostring(CallerIpAddress), ResourceId=tostring(_ResourceId) + | project TimeGenerated, CallerUPN, SourceIP, ResourceId, ResourceGroup, + SubscriptionId, CorrelationId, OperationNameValue +entityMappings: + - entityType: Account + fieldMappings: [{identifier: FullName, columnName: CallerUPN}] + - entityType: IP + fieldMappings: [{identifier: Address, columnName: SourceIP}] + - entityType: AzureResource + fieldMappings: [{identifier: ResourceId, columnName: ResourceId}] +customDetails: {Operation: OperationNameValue, ResourceGroup: ResourceGroup, CorrelationId: CorrelationId} +alertDetailsOverride: + alertDisplayNameFormat: "Azure resource deleted by {{CallerUPN}}" + alertDescriptionFormat: "{{OperationNameValue}} completed successfully for {{ResourceId}}." +eventGroupingSettings: {aggregationKind: AlertPerResult} +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: Selected + groupByEntities: [Account, AzureResource] + groupByAlertDetails: [] + groupByCustomDetails: [] +suppressionDuration: 5h +suppressionEnabled: false +version: 1.0.0 +kind: Scheduled diff --git a/analytics-rules/endpoint/suspicious-powershell.yaml b/analytics-rules/endpoint/suspicious-powershell.yaml new file mode 100644 index 0000000..9075828 --- /dev/null +++ b/analytics-rules/endpoint/suspicious-powershell.yaml @@ -0,0 +1,47 @@ +id: c3ab5008-c8ff-45cd-9a69-d2d843d7e17f +name: Suspicious PowerShell execution +description: Detects PowerShell command lines containing encoded-command, download, expression, bypass, or hidden-window indicators. Tune approved automation. +severity: High +status: Available +requiredDataConnectors: + - connectorId: MicrosoftThreatProtection + dataTypes: [DeviceProcessEvents] +queryFrequency: 15m +queryPeriod: 30m +triggerOperator: gt +triggerThreshold: 0 +tactics: [Execution] +relevantTechniques: [T1059.001] +query: | + let SuspiciousFlags = dynamic(["-enc", "-encodedcommand", "downloadstring", "iex", "invoke-expression", "bypass", "hidden"]); + DeviceProcessEvents + | where Timestamp > ago(30m) + | where FileName in~ ("powershell.exe", "pwsh.exe") + | where ProcessCommandLine has_any (SuspiciousFlags) + | extend TimeGenerated=Timestamp, AccountUPN=InitiatingProcessAccountUpn + | project TimeGenerated, DeviceName, DeviceId, AccountUPN, FileName, + ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine +entityMappings: + - entityType: Host + fieldMappings: [{identifier: HostName, columnName: DeviceName}] + - entityType: Account + fieldMappings: [{identifier: FullName, columnName: AccountUPN}] +customDetails: {CommandLine: ProcessCommandLine, ParentProcess: InitiatingProcessFileName} +alertDetailsOverride: + alertDisplayNameFormat: "Suspicious PowerShell on {{DeviceName}}" + alertDescriptionFormat: "PowerShell was launched by {{InitiatingProcessFileName}} with suspicious content." +eventGroupingSettings: {aggregationKind: AlertPerResult} +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: Selected + groupByEntities: [Host, Account] + groupByAlertDetails: [] + groupByCustomDetails: [] +suppressionDuration: 5h +suppressionEnabled: false +version: 1.0.0 +kind: Scheduled diff --git a/analytics-rules/identity/password-spray.yaml b/analytics-rules/identity/password-spray.yaml new file mode 100644 index 0000000..de18bfb --- /dev/null +++ b/analytics-rules/identity/password-spray.yaml @@ -0,0 +1,47 @@ +id: b7156e30-90f4-47db-83bf-6fd08e5097b8 +name: Password spray against multiple Entra ID users +description: Detects invalid-credential failures from one source IP across many users. Tune result codes, thresholds, and exclusions for the tenant. +severity: High +status: Available +requiredDataConnectors: + - connectorId: AzureActiveDirectory + dataTypes: [SigninLogs] +queryFrequency: 30m +queryPeriod: 1h +triggerOperator: gt +triggerThreshold: 0 +tactics: [CredentialAccess] +relevantTechniques: [T1110.003] +query: | + let UserThreshold = 15; + let InvalidCredentialCodes = dynamic(["50126", "50053", "50055", "50056"]); + SigninLogs + | where TimeGenerated > ago(1h) + | where tostring(ResultType) in (InvalidCredentialCodes) + | summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), + FailedUsers=dcount(UserPrincipalName), Users=make_set(UserPrincipalName, 100), + Apps=make_set(AppDisplayName, 50) by IPAddress, bin(TimeGenerated, 30m) + | where FailedUsers >= UserThreshold + | project TimeGenerated, StartTime, EndTime, IPAddress, FailedUsers, Users, Apps +entityMappings: + - entityType: IP + fieldMappings: [{identifier: Address, columnName: IPAddress}] +customDetails: {FailedUsers: FailedUsers, StartTime: StartTime, EndTime: EndTime} +alertDetailsOverride: + alertDisplayNameFormat: "Password spray from {{IPAddress}}" + alertDescriptionFormat: "The source attempted authentication against {{FailedUsers}} users." +eventGroupingSettings: {aggregationKind: AlertPerResult} +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: Selected + groupByEntities: [IP] + groupByAlertDetails: [] + groupByCustomDetails: [] +suppressionDuration: 5h +suppressionEnabled: false +version: 2.0.0 +kind: Scheduled diff --git a/analytics-rules/purview/ai-assisted-data-exfiltration.yaml b/analytics-rules/purview/ai-assisted-data-exfiltration.yaml new file mode 100644 index 0000000..df4274e --- /dev/null +++ b/analytics-rules/purview/ai-assisted-data-exfiltration.yaml @@ -0,0 +1,58 @@ +id: 8b059d67-d308-4569-920b-e516b0a5e6e0 +name: AI activity correlated with mass file operations +description: Correlates custom AI activity with high-volume SharePoint or OneDrive access. Correlation establishes proximity, not causation; validate schema and business context. +severity: High +status: Available +requiredDataConnectors: + - connectorId: Office365 + dataTypes: [OfficeActivity] + - connectorId: CustomLogsAma + dataTypes: [AIApp_CL] +queryFrequency: 1h +queryPeriod: 2h +triggerOperator: gt +triggerThreshold: 0 +tactics: [Collection, Exfiltration] +relevantTechniques: [T1530] +query: | + let FileEventThreshold = 100; + let AIUsers = AIApp_CL + | where TimeGenerated > ago(2h) + | summarize AIPromptCount=count(), AISessions=make_set(SessionId_s, 50), + AIStart=min(TimeGenerated), AIEnd=max(TimeGenerated) by UserPrincipalName=tostring(UserPrincipalName_s); + OfficeActivity + | where TimeGenerated > ago(2h) + | where Workload in ("SharePoint", "OneDrive") + | where Operation in ("FileAccessed", "FileDownloaded", "FileSyncDownloadedFull", "AnonymousLinkCreated", "SharingSet") + | summarize FileEvents=count(), Operations=make_set(Operation, 50), Sites=make_set(Site_Url, 25), + SourceIPs=make_set(ClientIP, 20), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated) + by UserPrincipalName=tostring(UserId), bin(TimeGenerated, 30m) + | where FileEvents >= FileEventThreshold + | join kind=inner AIUsers on UserPrincipalName + | extend PrimarySourceIP=tostring(SourceIPs[0]) + | project TimeGenerated, UserPrincipalName, PrimarySourceIP, FileEvents, Operations, + Sites, AIPromptCount, AISessions, AIStart, AIEnd, StartTime, EndTime +entityMappings: + - entityType: Account + fieldMappings: [{identifier: FullName, columnName: UserPrincipalName}] + - entityType: IP + fieldMappings: [{identifier: Address, columnName: PrimarySourceIP}] +customDetails: {FileEvents: FileEvents, AIPromptCount: AIPromptCount, Sites: Sites} +alertDetailsOverride: + alertDisplayNameFormat: "AI and mass file activity by {{UserPrincipalName}}" + alertDescriptionFormat: "{{FileEvents}} file events were correlated with {{AIPromptCount}} AI prompts." +eventGroupingSettings: {aggregationKind: AlertPerResult} +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: Selected + groupByEntities: [Account] + groupByAlertDetails: [] + groupByCustomDetails: [] +suppressionDuration: 5h +suppressionEnabled: false +version: 2.0.0 +kind: Scheduled diff --git a/analytics-rules/ueba/privileged-identity-anomaly.yaml b/analytics-rules/ueba/privileged-identity-anomaly.yaml new file mode 100644 index 0000000..422eedb --- /dev/null +++ b/analytics-rules/ueba/privileged-identity-anomaly.yaml @@ -0,0 +1,57 @@ +id: 8ffd382b-27d2-458e-86d6-b530dcf7375b +name: High-priority UEBA anomaly involving a privileged identity +description: Correlates high-priority behavioral anomalies with privileged-role or blast-radius context. Adapt role terms and IdentityInfo fields to the portal schema. +severity: High +status: Available +requiredDataConnectors: + - connectorId: BehaviorAnalytics + dataTypes: [BehaviorAnalytics, IdentityInfo] +queryFrequency: 1h +queryPeriod: 1h +triggerOperator: gt +triggerThreshold: 0 +tactics: [PrivilegeEscalation, DefenseEvasion] +relevantTechniques: [T1078] +query: | + let MinimumPriority = 7; + let PrivilegedRoleTerms = dynamic(["admin", "privileged", "security operator", "global reader"]); + let LatestIdentity = IdentityInfo + | where TimeGenerated > ago(14d) + | extend IdentityUPN=tostring(column_ifexists("AccountUPN", "")), + Roles=tostring(column_ifexists("AssignedRoles", "")), + BlastRadius=tostring(column_ifexists("BlastRadius", "")) + | where isnotempty(IdentityUPN) + | summarize arg_max(TimeGenerated, *) by IdentityUPN; + BehaviorAnalytics + | where TimeGenerated > ago(1h) and InvestigationPriority >= MinimumPriority + | join kind=inner LatestIdentity on $left.UserPrincipalName == $right.IdentityUPN + | where Roles has_any (PrivilegedRoleTerms) or BlastRadius =~ "High" + | extend AccountUPN=UserPrincipalName, SourceIP=SourceIPAddress, AnomalyReason=tostring(ActivityInsights) + | project TimeGenerated, AccountUPN, SourceIP, SourceDevice, InvestigationPriority, + ActivityType, Roles, BlastRadius, AnomalyReason +entityMappings: + - entityType: Account + fieldMappings: [{identifier: FullName, columnName: AccountUPN}] + - entityType: IP + fieldMappings: [{identifier: Address, columnName: SourceIP}] + - entityType: Host + fieldMappings: [{identifier: HostName, columnName: SourceDevice}] +customDetails: {Priority: InvestigationPriority, ActivityType: ActivityType, Roles: Roles, BlastRadius: BlastRadius} +alertDetailsOverride: + alertDisplayNameFormat: "Privileged identity anomaly: {{AccountUPN}}" + alertDescriptionFormat: "{{ActivityType}} received investigation priority {{InvestigationPriority}}." +eventGroupingSettings: {aggregationKind: AlertPerResult} +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: Selected + groupByEntities: [Account] + groupByAlertDetails: [] + groupByCustomDetails: [] +suppressionDuration: 5h +suppressionEnabled: false +version: 1.0.0 +kind: Scheduled diff --git a/deployment/DEPLOYMENT-COMMANDS.md b/deployment/DEPLOYMENT-COMMANDS.md deleted file mode 100644 index 4774752..0000000 --- a/deployment/DEPLOYMENT-COMMANDS.md +++ /dev/null @@ -1,139 +0,0 @@ -# Deployment Commands - -## 1. Deploy GitHub Profile README - -Create a repository named exactly: - -```text -leandroer -``` - -Then deploy: - -```bash -cd profile-readme/leandroer - -git init -git branch -M main -git remote add origin https://github.com/leandroer/leandroer.git - -git add README.md -git commit -m "Add professional GitHub profile README" -git push -u origin main -``` - -If the repo already exists: - -```bash -git remote set-url origin https://github.com/leandroer/leandroer.git -git push -u origin main --force -``` - -## 2. Deploy Sentinel Analytics Rules Repository - -Create the repo: - -```bash -gh repo create Sentinel-Analytics-Rules --public -``` - -Or create it manually at: - -```text -https://github.com/new -``` - -Then deploy: - -```bash -cd Sentinel-Analytics-Rules - -git init -git branch -M main -git remote add origin https://github.com/leandroer/Sentinel-Analytics-Rules.git - -git add . -git commit -m "Add Sentinel Analytics Rules starter framework" -git push -u origin main -``` - -If GitHub says fetch first and the repository is new: - -```bash -git push -u origin main --force -``` - -## 3. Deploy Sentinel Workbooks Repository - -Create the repo: - -```bash -gh repo create Sentinel-Workbooks --public -``` - -Then deploy: - -```bash -cd Sentinel-Workbooks - -git init -git branch -M main -git remote add origin https://github.com/leandroer/Sentinel-Workbooks.git - -git add . -git commit -m "Add Sentinel Workbooks starter framework" -git push -u origin main -``` - -## 4. Copy Standard Templates Into Existing Repositories - -For each existing repo: - -```bash -cp repo-templates/DETECTION-DOCUMENTATION-TEMPLATE.md /docs/ -cp repo-templates/PROJECT-ROADMAP.md /ROADMAP.md -``` - -Then commit: - -```bash -git add . -git commit -m "Add standard project documentation templates" -git push -``` - -## 5. Add Architecture Diagrams - -Copy diagrams into your repos: - -```bash -cp diagrams/ai-security-operations-architecture.md AI-Security-Incident-Response-Lab/docs/ -cp diagrams/sentinel-soar-architecture.md IncidentResponse/docs/ -cp diagrams/snort-detection-architecture.md Snort-Detection-Engineering-Lab/docs/ -``` - -Then commit and push. - -## GitHub Token Reminder - -If GitHub asks for a password, paste a Personal Access Token, not your GitHub password. - -## Recommended Repository Descriptions - -### Sentinel-Analytics-Rules - -```text -Professional Microsoft Sentinel analytics rules for Identity, Microsoft Purview, AI Security, Microsoft Agent 365, Defender XDR, and SOC detection engineering. -``` - -### Sentinel-Workbooks - -```text -Microsoft Sentinel workbook concepts for SOC operations, incident response, AI Security, Microsoft Purview, and automation monitoring. -``` - -### leandroer Profile README - -```text -Professional GitHub profile README for cybersecurity portfolio visibility. -``` diff --git a/deployment/QUICK-COMMANDS.sh b/deployment/QUICK-COMMANDS.sh deleted file mode 100644 index 8494a72..0000000 --- a/deployment/QUICK-COMMANDS.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -echo "This script shows deployment commands. Run each section manually from the correct folder." -echo -echo "Deploy profile README:" -echo "cd profile-readme/leandroer && git init && git branch -M main && git remote add origin https://github.com/leandroer/leandroer.git && git add README.md && git commit -m 'Add professional GitHub profile README' && git push -u origin main" -echo -echo "Deploy Sentinel Analytics Rules:" -echo "cd Sentinel-Analytics-Rules && git init && git branch -M main && git remote add origin https://github.com/leandroer/Sentinel-Analytics-Rules.git && git add . && git commit -m 'Add Sentinel Analytics Rules starter framework' && git push -u origin main" -echo -echo "Deploy Sentinel Workbooks:" -echo "cd Sentinel-Workbooks && git init && git branch -M main && git remote add origin https://github.com/leandroer/Sentinel-Workbooks.git && git add . && git commit -m 'Add Sentinel Workbooks starter framework' && git push -u origin main" diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000..9f41d7d --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,17 @@ +# Deployment Guidance + +The YAML files are source-controlled analytics-rule examples, not a guarantee of one-click tenant deployment. + +## Recommended process + +1. Review the rule and its custom-schema dependencies. +2. Confirm that required tables contain recent representative data. +3. Run the KQL in Microsoft Sentinel Logs and resolve schema differences. +4. Create the rule disabled or in a non-production workspace. +5. Verify entities, custom details, dynamic text, alert grouping, and incident grouping. +6. Run controlled positive and negative scenarios. +7. Record evidence before enabling production incident creation. + +For repeatable deployment, translate validated rules into an approved ARM, Bicep, or Microsoft Sentinel solution packaging workflow. Keep tenant IDs, workspace IDs, credentials, and environment-specific exclusions outside this public repository. + +Do not use force-push bootstrap scripts or long-lived Azure credentials. Use protected environments and workload identity federation when deployment automation is introduced. diff --git a/diagrams/ai-security-operations-architecture.md b/diagrams/ai-security-operations-architecture.md deleted file mode 100644 index caa775f..0000000 --- a/diagrams/ai-security-operations-architecture.md +++ /dev/null @@ -1,21 +0,0 @@ -# AI Security Operations Architecture - -```mermaid -flowchart TD - A[Microsoft Agent 365] --> S[Microsoft Sentinel] - B[Microsoft Purview] --> S - C[Microsoft 365 Copilot] --> S - D[Security Copilot] --> S - E[Azure OpenAI / AI Foundry] --> S - F[Microsoft Defender XDR] --> S - G[Microsoft Entra ID] --> S - - S --> H[AI Security Analytics Rules] - H --> I[Sentinel Incident] - I --> J[Logic App Playbook] - J --> K[Teams Notification] - J --> L[Email SOC] - J --> M[Disable Agent / Connector] - J --> N[Revoke Session] - J --> O[Update Incident] -``` diff --git a/diagrams/sentinel-soar-architecture.md b/diagrams/sentinel-soar-architecture.md deleted file mode 100644 index 81cff2b..0000000 --- a/diagrams/sentinel-soar-architecture.md +++ /dev/null @@ -1,17 +0,0 @@ -# Sentinel SOAR Architecture - -```mermaid -flowchart LR - A[Security Telemetry] --> B[Microsoft Sentinel] - B --> C[Analytics Rule] - C --> D[Incident] - D --> E[Logic App Playbook] - - E --> F[Assign Owner] - E --> G[Add Tags] - E --> H[Teams Adaptive Card] - E --> I[Email Notification] - E --> J[Defender Action] - E --> K[Entra Response] - E --> L[Incident Comment] -``` diff --git a/diagrams/snort-detection-architecture.md b/diagrams/snort-detection-architecture.md deleted file mode 100644 index ec43d1e..0000000 --- a/diagrams/snort-detection-architecture.md +++ /dev/null @@ -1,11 +0,0 @@ -# Snort Detection Architecture - -```mermaid -flowchart TD - A[Test Traffic / Network TAP / SPAN Port] --> B[Snort Sensor] - B --> C[Snort Rules] - C --> D[Alerts] - D --> E[SIEM / Log Review] - E --> F[SOC Analyst] - F --> G[Incident Response] -``` diff --git a/docs/architecture/detection-lifecycle.md b/docs/architecture/detection-lifecycle.md new file mode 100644 index 0000000..f56bb51 --- /dev/null +++ b/docs/architecture/detection-lifecycle.md @@ -0,0 +1,14 @@ +# Detection Lifecycle + +```mermaid +flowchart LR + H["Hunt"] --> V["Validate"] + V --> T["Tune"] + T --> R["Rule"] + R --> L["Lab deployment"] + L --> M["Measure"] + M --> I["Improve"] + I --> T +``` + +A detection is mature only when its telemetry assumptions, expected positives, expected benign activity, alert behavior, response ownership, and known blind spots are documented and periodically reviewed. diff --git a/docs/coverage-matrix.md b/docs/coverage-matrix.md new file mode 100644 index 0000000..3c0386a --- /dev/null +++ b/docs/coverage-matrix.md @@ -0,0 +1,13 @@ +# Coverage Matrix + +| Rule | Identity | Endpoint | Cloud | Data | AI/Agent | Framework | +|---|---:|---:|---:|---:|---:|---| +| Password spray | ✓ | | | | | ATT&CK T1110.003 | +| Suspicious PowerShell | ✓ | ✓ | | | | ATT&CK T1059.001 | +| Azure resource deletion | ✓ | | ✓ | | | ATT&CK T1485 | +| AI and mass file activity | ✓ | | ✓ | ✓ | ✓ | ATT&CK T1530 | +| Prompt-injection indicators | ✓ | | | | ✓ | OWASP LLM01 | +| Unauthorized agent tool use | ✓ | | ✓ | ✓ | ✓ | ATT&CK T1059; agent-risk context | +| Privileged-identity anomaly | ✓ | | ✓ | | | ATT&CK T1078 | + +Blank cells are intentional visibility gaps rather than claims of complete coverage. Future rules should be chosen to close meaningful data-source and behavioral gaps, not merely increase rule count. diff --git a/docs/rule-authoring-guide.md b/docs/rule-authoring-guide.md new file mode 100644 index 0000000..4da637b --- /dev/null +++ b/docs/rule-authoring-guide.md @@ -0,0 +1,29 @@ +# Analytics Rule Authoring Guide + +## Lifecycle + +1. Define the adversary behavior and required telemetry. +2. Write a broad hunting query. +3. Validate against 7–30 days of representative data. +4. Test positive, negative, and threshold-boundary cases. +5. Tune exclusions and preserve known coverage gaps. +6. Map entities, custom details, and dynamic alert text. +7. Configure alert and incident grouping deliberately. +8. Deploy disabled or into a lab workspace. +9. Measure precision, volume, and investigation value. +10. Version and review the rule periodically. + +## Requirements + +- Filter time early and return `TimeGenerated` in the final output. +- Project every entity, custom-detail, and alert-template field. +- Bound `make_set()` and similar aggregations. +- Keep `queryFrequency` less than or equal to `queryPeriod`. +- Document connector and custom-schema assumptions. +- Do not force an Enterprise ATT&CK mapping when no defensible mapping exists. +- Treat keyword matches and UEBA scores as context rather than proof. +- Use stable GUIDs and semantic versions. + +## Validation status + +Rules are examples until evidence supports promotion to Lab Tested or Production Validated. Static YAML validation is necessary but does not compile KQL or prove tenant schema compatibility. diff --git a/docs/rule-catalog.md b/docs/rule-catalog.md new file mode 100644 index 0000000..7a9b513 --- /dev/null +++ b/docs/rule-catalog.md @@ -0,0 +1,13 @@ +# Analytics Rule Catalog + +Generated by `scripts/generate_catalog.py`; do not edit manually. + +| Rule | Domain | Severity | Tables | ATT&CK | Entities | Version | +|---|---|---|---|---|---|---| +| [Risky or unauthorized AI agent tool execution](../analytics-rules/agent-365/unauthorized-agent-tool-use.yaml) | agent-365 | High | AIToolExecution_CL | T1059 | Account, IP | 2.0.0 | +| [Repeated prompt-injection indicators](../analytics-rules/ai-security/prompt-injection.yaml) | ai-security | High | AIApp_CL | — | Account, IP | 2.0.0 | +| [Successful Azure resource deletion](../analytics-rules/cloud/azure-resource-deletion.yaml) | cloud | Medium | AzureActivity | T1485 | Account, IP, AzureResource | 1.0.0 | +| [Suspicious PowerShell execution](../analytics-rules/endpoint/suspicious-powershell.yaml) | endpoint | High | DeviceProcessEvents | T1059.001 | Host, Account | 1.0.0 | +| [Password spray against multiple Entra ID users](../analytics-rules/identity/password-spray.yaml) | identity | High | SigninLogs | T1110.003 | IP | 2.0.0 | +| [AI activity correlated with mass file operations](../analytics-rules/purview/ai-assisted-data-exfiltration.yaml) | purview | High | AIApp_CL, OfficeActivity | T1530 | Account, IP | 2.0.0 | +| [High-priority UEBA anomaly involving a privileged identity](../analytics-rules/ueba/privileged-identity-anomaly.yaml) | ueba | High | BehaviorAnalytics, IdentityInfo | T1078 | Account, IP, Host | 1.0.0 | diff --git a/docs/schemas/AIApp_CL.md b/docs/schemas/AIApp_CL.md new file mode 100644 index 0000000..6e79ed4 --- /dev/null +++ b/docs/schemas/AIApp_CL.md @@ -0,0 +1,16 @@ +# `AIApp_CL` Contract + +`AIApp_CL` is a repository-defined custom table, not a built-in Sentinel table. + +Required fields: + +| Column | Type | Purpose | +|---|---|---| +| `TimeGenerated` | `datetime` | Event time | +| `UserPrincipalName_s` | `string` | User or workload identity | +| `SourceIP_s` | `string` | Source address | +| `SessionId_s` | `string` | AI session | +| `Application_s` | `string` | AI application | +| `Prompt_s` | `string` | Submitted prompt | + +Prompts can contain credentials, personal information, or proprietary data. Minimize collection, redact secrets, restrict access, and document retention before ingestion. Normalize DCR-created columns without legacy suffixes at ingestion or with a parser. diff --git a/docs/schemas/AIToolExecution_CL.md b/docs/schemas/AIToolExecution_CL.md new file mode 100644 index 0000000..262faf4 --- /dev/null +++ b/docs/schemas/AIToolExecution_CL.md @@ -0,0 +1,18 @@ +# `AIToolExecution_CL` Contract + +`AIToolExecution_CL` is a repository-defined custom table for agent, connector, plugin, and function execution. + +Required fields: + +| Column | Type | Purpose | +|---|---|---| +| `TimeGenerated` | `datetime` | Execution time | +| `UserPrincipalName_s` | `string` | Requesting identity | +| `SourceIP_s` | `string` | Source address | +| `AgentId_s` | `string` | Agent identifier | +| `ToolName_s` | `string` | Tool or connector | +| `Result_s` | `string` | Execution result | +| `RiskLevel_s` | `string` | Environment-defined risk | +| `ApprovalStatus_s` | `string` | Approval state | + +Do not ingest secrets, full tokens, authorization headers, or unredacted sensitive parameters. Preserve correlation from session through approval, execution, and result. diff --git a/docs/validation-strategy.md b/docs/validation-strategy.md new file mode 100644 index 0000000..76f92da --- /dev/null +++ b/docs/validation-strategy.md @@ -0,0 +1,24 @@ +# Validation Strategy + +```mermaid +flowchart LR + S["Static validation"] --> F["KQL fixtures"] + F --> C["Workspace compilation"] + C --> D["Disabled rule deployment"] + D --> A["Alert and incident verification"] + A --> M["Operational measurement"] +``` + +## Static checks + +`scripts/validate_rules.py` verifies YAML, GUIDs, schedule coverage, `TimeGenerated`, final output columns, dynamic alert fields, and fixtures. + +## Logic fixtures + +Self-contained `datatable()` fixtures exercise transparent pieces of detection logic. They complement—but do not replace—connector-backed testing. + +## Workspace validation + +Compile each query in a non-production Sentinel workspace with the declared tables and parsers. Confirm field types, ingestion delays, mapped entities, custom details, grouping, and expected positive and negative outcomes. + +Workspace integration is not enabled in the public workflow because it requires an authorized Azure tenant, test workspace, populated connectors, controlled data, and protected OIDC configuration. diff --git a/profile-readme/leandroer/README.md b/profile-readme/leandroer/README.md deleted file mode 100644 index 28108ed..0000000 --- a/profile-readme/leandroer/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Leandro Rocha - -Cybersecurity professional focused on **Incident Response**, **Detection Engineering**, **Threat Hunting**, **Microsoft Sentinel**, **Microsoft Defender XDR**, **SOAR Automation**, and **AI Security Operations**. - -## Focus Areas - -- Incident Response modernization -- Detection engineering with KQL, Sigma, and Snort -- Microsoft Sentinel analytics rules -- Microsoft Defender XDR investigations -- Microsoft Purview data security and exfiltration correlation -- Microsoft Agent 365 and AI agent security -- Microsoft 365 Copilot and Security Copilot monitoring -- Azure Logic Apps / SOAR automation -- Threat hunting and SOC enablement - -## Featured Projects - -| Project | Description | -|---|---| -| [IncidentResponse](https://github.com/leandroer/IncidentResponse) | Professional incident response website and technical IR reference | -| [AI-Security-Incident-Response-Lab](https://github.com/leandroer/AI-Security-Incident-Response-Lab) | AI Security IR framework mapped to OWASP GenAI Top 10, Microsoft Agent 365, Purview, Sentinel, and Logic Apps | -| [KQL-Templates](https://github.com/leandroer/KQL-Templates) | KQL templates, hunting queries, detection engineering patterns, and analyst learning resources | -| [Snort-Detection-Engineering-Lab](https://github.com/leandroer/Snort-Detection-Engineering-Lab) | Snort 3 detection engineering, IDS/IPS deployment, packet analysis, and network threat detection | -| [Sentinel-Analytics-Rules](https://github.com/leandroer/Sentinel-Analytics-Rules) | Microsoft Sentinel analytics rules for identity, data security, AI security, and agentic threats | -| [Sigma-Templates](https://github.com/leandroer/Sigma-Templates) | Sigma detection templates and portable detection logic | - -## Security Engineering Portfolio - -```mermaid -flowchart LR - A[Detection Engineering] --> B[Threat Hunting] - B --> C[Incident Response] - C --> D[SOAR Automation] - D --> E[AI Security Operations] - - A --> F[KQL] - A --> G[Sigma] - A --> H[Snort] - D --> I[Logic Apps] - E --> J[Microsoft Agent 365] - E --> K[Microsoft Purview] - E --> L[Microsoft Sentinel] -``` - -## Core Technologies - -![Microsoft Sentinel](https://img.shields.io/badge/Microsoft%20Sentinel-SIEM-blue) -![Microsoft Defender XDR](https://img.shields.io/badge/Microsoft%20Defender-XDR-blue) -![Microsoft Purview](https://img.shields.io/badge/Microsoft%20Purview-Data%20Security-blue) -![KQL](https://img.shields.io/badge/KQL-Detection%20Engineering-purple) -![AI Security](https://img.shields.io/badge/AI%20Security-GenAI%20%7C%20Agentic%20AI-orange) -![Incident Response](https://img.shields.io/badge/Incident%20Response-Blue%20Team-red) - -## Current Portfolio Direction - -I am building practical, technical, and professional security engineering projects that help defenders understand, detect, investigate, and respond to modern threats across cloud, identity, endpoint, network, data, and AI systems. - -## Contact - -- GitHub: [leandroer](https://github.com/leandroer) diff --git a/repo-templates/DETECTION-DOCUMENTATION-TEMPLATE.md b/repo-templates/DETECTION-DOCUMENTATION-TEMPLATE.md deleted file mode 100644 index db99e91..0000000 --- a/repo-templates/DETECTION-DOCUMENTATION-TEMPLATE.md +++ /dev/null @@ -1,58 +0,0 @@ -# Detection Name - -## Summary - -Briefly describe the behavior being detected. - -## Threat Scenario - -Explain the attack, misuse, or suspicious behavior. - -## Required Data Sources - -- Table/log source 1 -- Table/log source 2 - -## Detection Logic - -```kql -// Query goes here -``` - -## Expected Output - -| Field | Description | -|---|---| -| TimeGenerated | Event time | -| UserPrincipalName | User involved | -| SourceIP | Source IP address | -| DetectionName | Detection title | -| Severity | Detection severity | - -## False Positives - -- Legitimate admin activity -- Vulnerability scanners -- Automation jobs -- Business-approved testing - -## Tuning Guidance - -- Adjust thresholds based on baseline. -- Add approved admin accounts. -- Exclude known scanners. -- Restrict to critical assets if needed. - -## Response Guidance - -1. Validate the entity. -2. Review recent activity. -3. Correlate with identity, endpoint, cloud, and data logs. -4. Contain if malicious. -5. Document incident findings. - -## MITRE ATT&CK Mapping - -| Tactic | Technique | -|---|---| -| Initial Access | Example | diff --git a/repo-templates/PROJECT-ROADMAP.md b/repo-templates/PROJECT-ROADMAP.md deleted file mode 100644 index a8b78d3..0000000 --- a/repo-templates/PROJECT-ROADMAP.md +++ /dev/null @@ -1,29 +0,0 @@ -# Project Roadmap - -## Version 1.0 - -- Initial documentation -- Core detections -- Deployment guide -- Response guidance - -## Version 1.1 - -- Additional detection categories -- Architecture diagrams -- Analyst workflow -- Tuning guidance - -## Version 1.2 - -- Analytics rule YAML -- SOAR playbooks -- Workbook examples -- MITRE mapping - -## Version 2.0 - -- End-to-end lab deployment -- Sample datasets -- Automated validation -- CI checks for query/rule quality diff --git a/repo-templates/STANDARD-REPO-README.md b/repo-templates/STANDARD-REPO-README.md deleted file mode 100644 index aa7beec..0000000 --- a/repo-templates/STANDARD-REPO-README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Project Name - -Professional one-sentence summary explaining what the project does and who it helps. - -## Overview - -Explain the project purpose, target audience, and security value. - -## Who This Is For - -| Role | Value | -|---|---| -| SOC Analyst | Investigation and triage guidance | -| Detection Engineer | Detection logic and tuning | -| Incident Responder | Response workflow | -| Security Architect | Telemetry and architecture guidance | - -## Architecture - -```mermaid -flowchart LR - A[Telemetry Sources] --> B[Detection Logic] - B --> C[SIEM] - C --> D[Incident] - D --> E[Automation] - E --> F[Response] -``` - -## Repository Structure - -```text -. -├── docs/ -├── detections/ -├── playbooks/ -├── scripts/ -└── README.md -``` - -## Getting Started - -```bash -git clone -cd -``` - -## Detection Engineering Content - -Each detection should include: - -- Objective -- Required data sources -- Query/rule logic -- Expected output -- False positives -- Tuning guidance -- Response guidance - -## Disclaimer - -This project is intended for defensive security operations, detection engineering, and incident response education. diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..8392d54 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +PyYAML==6.0.2 diff --git a/scripts/generate_catalog.py b/scripts/generate_catalog.py new file mode 100644 index 0000000..9a2db09 --- /dev/null +++ b/scripts/generate_catalog.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Generate the analytics-rule catalog.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import yaml + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = ROOT / "docs" / "rule-catalog.md" + + +def build() -> str: + lines = ["# Analytics Rule Catalog", "", "Generated by `scripts/generate_catalog.py`; do not edit manually.", "", + "| Rule | Domain | Severity | Tables | ATT&CK | Entities | Version |", + "|---|---|---|---|---|---|---|"] + for path in sorted((ROOT / "analytics-rules").rglob("*.yaml")): + rule = yaml.safe_load(path.read_text(encoding="utf-8")) + rel = path.relative_to(ROOT).as_posix() + tables = sorted({table for item in rule["requiredDataConnectors"] for table in item.get("dataTypes", [])}) + entities = [item["entityType"] for item in rule.get("entityMappings", [])] + values = [f"[{rule['name']}](../{rel})", path.parent.name, rule["severity"], ", ".join(tables), + ", ".join(rule.get("relevantTechniques", [])) or "—", ", ".join(entities), rule["version"]] + lines.append("| " + " | ".join(str(v).replace("|", "\\|") for v in values) + " |") + return "\n".join(lines) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + content = build() + if args.check: + if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != content: + print("Rule catalog is stale. Run: python3 scripts/generate_catalog.py") + return 1 + print("Rule catalog is current.") + return 0 + OUTPUT.write_text(content, encoding="utf-8") + print(f"Generated {OUTPUT.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate_rules.py b/scripts/validate_rules.py new file mode 100644 index 0000000..a0a62aa --- /dev/null +++ b/scripts/validate_rules.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Validate Microsoft Sentinel scheduled analytics-rule examples.""" + +from __future__ import annotations + +import re +import sys +import uuid +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +RULE_ROOT = ROOT / "analytics-rules" +REQUIRED = ( + "id", "name", "description", "severity", "status", "requiredDataConnectors", + "queryFrequency", "queryPeriod", "triggerOperator", "triggerThreshold", "tactics", + "relevantTechniques", "query", "entityMappings", "customDetails", + "eventGroupingSettings", "incidentConfiguration", "version", "kind", +) + + +def duration(value: object) -> int | None: + match = re.fullmatch(r"(\d+)([mhd])", str(value)) + if not match: + return None + return int(match.group(1)) * {"m": 1, "h": 60, "d": 1440}[match.group(2)] + + +def final_output(query: str) -> str: + projects = list(re.finditer(r"(?ms)^\s*\|\s*project\s+(.+?)(?=^\s*\||\Z)", query)) + return projects[-1].group(1) if projects else query + + +def main() -> int: + errors: list[str] = [] + ids: dict[str, Path] = {} + rules = sorted(RULE_ROOT.rglob("*.yaml")) + if not rules: + errors.append("analytics-rules: no rule YAML files found") + for path in rules: + rel = path.relative_to(ROOT).as_posix() + text = path.read_text(encoding="utf-8") + if "REPLACE-WITH" in text or "T0000" in text: + errors.append(f"{rel}: unresolved placeholder") + try: + rule = yaml.safe_load(text) + except yaml.YAMLError as exc: + errors.append(f"{rel}: invalid YAML: {exc}") + continue + for field in REQUIRED: + if field not in rule: + errors.append(f"{rel}: missing required field '{field}'") + rule_id = str(rule.get("id", "")) + try: + uuid.UUID(rule_id) + except ValueError: + errors.append(f"{rel}: id is not a valid GUID") + if rule_id in ids: + errors.append(f"{rel}: duplicate id also used by {ids[rule_id].relative_to(ROOT)}") + ids[rule_id] = path + query = str(rule.get("query", "")) + output = final_output(query) + if not re.search(r"\bTimeGenerated\b", output): + errors.append(f"{rel}: final query output must include TimeGenerated") + referenced: list[tuple[str, object]] = [] + for mapping in rule.get("entityMappings", []): + for field in mapping.get("fieldMappings", []): + referenced.append(("entity mapping", field.get("columnName"))) + referenced.extend(("custom detail", value) for value in rule.get("customDetails", {}).values()) + override = " ".join(str(v) for v in rule.get("alertDetailsOverride", {}).values()) + referenced.extend(("alert placeholder", value) for value in re.findall(r"\{\{\s*(\w+)\s*\}\}", override)) + for kind, column in referenced: + if column and not re.search(rf"\b{re.escape(str(column))}\b", output): + errors.append(f"{rel}: {kind} column '{column}' is not in the final output") + frequency, period = duration(rule.get("queryFrequency")), duration(rule.get("queryPeriod")) + if frequency is None or period is None: + errors.append(f"{rel}: schedule must use m, h, or d duration syntax") + elif frequency > period: + errors.append(f"{rel}: queryFrequency must not exceed queryPeriod") + for fixture in sorted((ROOT / "tests" / "fixtures").glob("*.test.kql")): + content = fixture.read_text(encoding="utf-8") + if "datatable(" not in content or "TestPassed" not in content: + errors.append(f"{fixture.relative_to(ROOT)}: fixture requires datatable() and TestPassed") + if errors: + print("\n".join(f"ERROR: {error}" for error in errors)) + return 1 + print(f"Validated {len(rules)} analytics rules.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/fixtures/password-spray.test.kql b/tests/fixtures/password-spray.test.kql new file mode 100644 index 0000000..afc48e9 --- /dev/null +++ b/tests/fixtures/password-spray.test.kql @@ -0,0 +1,15 @@ +// Self-contained threshold fixture. Expected output: TestPassed=true. +let UserThreshold = 3; +let InvalidCredentialCodes = dynamic(["50126"]); +let Input = datatable(TimeGenerated:datetime, IPAddress:string, UserPrincipalName:string, ResultType:string) +[ + datetime(2026-09-28T12:00:00Z), "203.0.113.10", "a@example.com", "50126", + datetime(2026-09-28T12:01:00Z), "203.0.113.10", "b@example.com", "50126", + datetime(2026-09-28T12:02:00Z), "203.0.113.10", "c@example.com", "50126", + datetime(2026-09-28T12:03:00Z), "198.51.100.10", "d@example.com", "50126" +]; +let Detection = Input +| where ResultType in (InvalidCredentialCodes) +| summarize FailedUsers=dcount(UserPrincipalName) by IPAddress +| where FailedUsers >= UserThreshold; +print TestPassed=toscalar(Detection | count) == 1 diff --git a/tests/fixtures/prompt-injection.test.kql b/tests/fixtures/prompt-injection.test.kql new file mode 100644 index 0000000..9ab9cf2 --- /dev/null +++ b/tests/fixtures/prompt-injection.test.kql @@ -0,0 +1,12 @@ +// Self-contained threshold fixture. Expected output: TestPassed=true. +let Threshold = 3; +let Terms = dynamic(["ignore previous instructions", "jailbreak"]); +let Input = datatable(TimeGenerated:datetime, User:string, Prompt:string) +[ + datetime(2026-09-28T12:00:00Z), "attacker@example.com", "ignore previous instructions", + datetime(2026-09-28T12:01:00Z), "attacker@example.com", "jailbreak", + datetime(2026-09-28T12:02:00Z), "attacker@example.com", "ignore previous instructions", + datetime(2026-09-28T12:03:00Z), "user@example.com", "summarize this report" +]; +let Detection = Input | where Prompt has_any (Terms) | summarize Attempts=count() by User | where Attempts >= Threshold; +print TestPassed=toscalar(Detection | count) == 1