-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.tf
More file actions
95 lines (81 loc) · 4.12 KB
/
Copy pathmain.tf
File metadata and controls
95 lines (81 loc) · 4.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# The schema guard: one place where every validation message from the pipeline in locals.tf fails
# the plan together, so an analyst sees every problem across every file in a single run instead of
# fixing them one apply at a time.
resource "terraform_data" "schema_guard" {
lifecycle {
precondition {
condition = length(local.schema_violations) == 0
error_message = "${length(local.schema_violations)} detection rule schema problem(s):\n - ${join("\n - ", local.schema_violations)}"
}
}
}
# In-graph remote validation: each rule's query runs against the tenant's advanced hunting schema
# through the Graph runHuntingQuery action before the rule itself is created or updated, so a query
# that references a missing table or column fails the apply here, with the rule named, instead of
# at rule create. `| take 1` caps the result set (validation needs schema soundness, not data) and
# the response schema, the query's output columns, is tracked in state so column changes surface.
# A query change replaces its action, re-validating exactly when it matters. Gated by
# remote_query_validation because the applying principal needs ThreatHunting.Read.All.
resource "msgraph_resource_action" "validate_queries" {
for_each = { for k, v in local.rule_bodies : k => v if var.remote_query_validation }
resource_url = "security"
action = "runHuntingQuery"
method = "POST"
api_version = var.hunting_api_version
body = {
Query = var.remote_validation_append_take ? "${each.value.queryCondition.queryText}\n| take 1" : each.value.queryCondition.queryText
Timespan = var.remote_validation_timespan
}
response_export_values = {
schema = "schema"
}
retry = var.retry_error_message_regex == null ? null : { error_message_regex = var.retry_error_message_regex }
depends_on = [terraform_data.schema_guard]
}
# One Graph custom detection rule per validated rule, from every source (baseline catalog, the
# analyst YAML directory, and custom_rules). The rule id is client provided (it doubles as the
# for_each key, so plans stay stable and re-creates never collide), and updates replace the full
# rule in place (update_method, PUT by default per the live PATCH rejection).
resource "msgraph_resource" "detection_rules" {
for_each = local.rule_bodies
url = "security/rules/detectionRules"
api_version = var.api_version
body = each.value
update_method = var.update_method
response_export_values = {
id = "id"
display_name = "displayName"
status = "status"
}
retry = var.retry_error_message_regex == null ? null : { error_message_regex = var.retry_error_message_regex }
# Rule deletion was observed hanging server side (live, via both the API and the portal), so the
# delete timeout keeps a wedged delete loud and bounded instead of spinning.
timeouts {
create = var.timeouts.create
delete = var.timeouts.delete
read = var.timeouts.read
update = var.timeouts.update
}
# The validation actions must complete first: a rule whose query cannot resolve against the
# tenant must never deploy. Proven live, where a rule create raced its failing validation before
# this dependency existed.
depends_on = [terraform_data.schema_guard, msgraph_resource_action.validate_queries]
}
# MITRE roll ups for the coverage outputs (and the CI coverage report), read straight from the
# authored mitre blocks rather than the deployed rules so a plan can report coverage too.
locals {
rule_tactics = {
for k, r in local.rules : k => distinct([
for m in try(concat(r.raw.alert.mitre, []), []) :
lookup(local.tactic_canonical, replace(lower(try(tostring(m.tactic), "unknown")), "/[ _-]/", ""), try(tostring(m.tactic), "unknown"))
])
}
rule_techniques = {
for k, r in local.rules : k => distinct(flatten([
for m in try(concat(r.raw.alert.mitre, []), []) : [
for t in try(concat(m.techniques, []), []) :
can(tostring(t)) ? [upper(tostring(t))] : concat([try(upper(tostring(t.technique)), "unknown")], [for st in try(concat(t.sub_techniques, []), []) : try(upper(tostring(st)), "unknown")])
]
]))
}
}