Skip to content

Commit b482699

Browse files
committed
implement fire_policy_webhooks
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent f8287c2 commit b482699

4 files changed

Lines changed: 70 additions & 23 deletions

File tree

notification/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class WebhookSubscriptionAdmin(ProhibitDataspaceLookupMixin, DataspacedAdmin):
6666
inlines = [WebhookDeliveryInline]
6767

6868
def get_inlines(self, request, obj=None):
69+
"""Exclude delivery history on the add form as no deliveries exist yet."""
6970
if obj is None:
7071
return []
7172
return super().get_inlines(request, obj)

notification/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"user.added_or_updated",
2929
"user.locked_out",
3030
"vulnerability.data_update",
31+
"policy.violation_detected",
32+
"policy.violation_resolved",
3133
]
3234

3335

policy/engine.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ def get_effective_config(rule_type, dataspace):
3333

3434

3535
def evaluate_rule(rule_type, product, threshold, parameters):
36-
"""Evaluate a single rule against a product and record the violation if triggered."""
36+
"""
37+
Evaluate a single rule against a product and record the violation if triggered.
38+
39+
Returns a 3-tuple: (violation_or_none, created, resolved_count).
40+
"""
3741
handler = RULE_REGISTRY[rule_type]
3842
violation_count = handler.count_violations(product, threshold, parameters)
3943

@@ -47,37 +51,44 @@ def evaluate_rule(rule_type, product, threshold, parameters):
4751
if not created:
4852
violation.violation_count = violation_count
4953
violation.save()
50-
return violation
54+
return violation, created, 0
5155

52-
else:
53-
ProductPolicyViolation.objects.filter(**lookup).update(
54-
resolved=True,
55-
resolved_date=timezone.now(),
56-
)
57-
return
56+
resolved_count = ProductPolicyViolation.objects.filter(**lookup).update(
57+
resolved=True,
58+
resolved_date=timezone.now(),
59+
)
60+
return None, False, resolved_count
5861

5962

6063
def evaluate_rules(product):
6164
"""
6265
Evaluate all rules in RULE_REGISTRY for the given product.
6366
64-
Returns the list of active ProductPolicyViolation instances.
67+
Returns a 2-tuple: (new_violations, resolved_count).
68+
new_violations is a list of newly created ProductPolicyViolation instances.
69+
resolved_count is the total number of violations resolved during this run.
6570
"""
66-
violations = []
71+
new_violations = []
72+
resolved_count = 0
73+
6774
for rule_type in RULE_REGISTRY:
6875
config = get_effective_config(rule_type, product.dataspace)
6976
if not config["is_active"]:
7077
# Explicitly resolve open violations so disabling a rule clears its history
7178
# rather than leaving stale unresolved records.
72-
ProductPolicyViolation.objects.filter(
79+
rows = ProductPolicyViolation.objects.filter(
7380
rule_type=rule_type,
7481
product=product,
7582
resolved=False,
7683
).update(resolved=True, resolved_date=timezone.now())
84+
resolved_count += rows
7785
continue
7886

79-
violation = evaluate_rule(rule_type, product, config["threshold"], config["parameters"])
80-
if violation:
81-
violations.append(violation)
87+
violation, created, resolved = evaluate_rule(
88+
rule_type, product, config["threshold"], config["parameters"]
89+
)
90+
if created:
91+
new_violations.append(violation)
92+
resolved_count += resolved
8293

83-
return violations
94+
return new_violations, resolved_count

policy/tasks.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,70 @@
1313
from django_rq import job
1414

1515
from dje.models import get_unsecured_manager
16+
from notification.models import fire_webhooks
1617
from policy.engine import evaluate_rules
1718

1819
logger = logging.getLogger(__name__)
1920

2021

22+
def fire_policy_webhooks(product, new_violations, resolved_count):
23+
"""Fire policy webhooks for newly detected or resolved violations."""
24+
if new_violations:
25+
lines = [
26+
f"- {violation.rule_label}: {violation.violation_count} violation(s)"
27+
for violation in new_violations
28+
]
29+
payload = {
30+
"text": (f"[DejaCode] Policy violations detected for {product}\n" + "\n".join(lines))
31+
}
32+
fire_webhooks("policy.violation_detected", instance=product, payload_override=payload)
33+
34+
if resolved_count:
35+
payload = {
36+
"text": (f"[DejaCode] {resolved_count} policy violation(s) resolved for {product}")
37+
}
38+
fire_webhooks("policy.violation_resolved", instance=product, payload_override=payload)
39+
40+
2141
@job
2242
def evaluate_product_rules_task(product_uuid):
23-
"""Evaluate all active PolicyRules for the given product."""
43+
"""Evaluate all active policy rules for the given product and fire webhooks on changes."""
2444
Product = apps.get_model("product_portfolio", "product")
2545

2646
try:
27-
product = Product.unsecured_objects.select_related("dataspace").get(uuid=product_uuid)
47+
product = get_unsecured_manager(Product).get(uuid=product_uuid)
2848
except Product.DoesNotExist:
2949
logger.error(f"evaluate_product_rules_task: product {product_uuid} not found, skipping.")
3050
return
3151

3252
logger.info(f"Evaluating policy rules for product {product}")
33-
violations = evaluate_rules(product)
34-
logger.info(f"Policy rules evaluated for {product}: {len(violations)} active violation(s).")
53+
new_violations, resolved_count = evaluate_rules(product)
54+
logger.info(
55+
f"Policy rules evaluated for {product}: "
56+
f"{len(new_violations)} new violation(s), {resolved_count} resolved."
57+
)
58+
fire_policy_webhooks(product, new_violations, resolved_count)
3559

3660

3761
@job
3862
def evaluate_all_products_rules_task(include_locked=False):
39-
"""Enqueue evaluate_product_rules_task for every product, skipping locked ones by default."""
63+
"""Evaluate policy rules for every product directly, skipping locked ones by default."""
4064
Product = apps.get_model("product_portfolio", "product")
4165

42-
products = Product.unsecured_objects.select_related("dataspace")
66+
products = get_unsecured_manager(Product).select_related("dataspace")
4367
if not include_locked:
4468
products = products.exclude_locked()
4569

4670
count = products.count()
47-
logger.info(f"Queuing policy rule evaluation for {count} product(s).")
71+
logger.info(f"Starting policy rule evaluation for {count} product(s).")
72+
4873
for product in products:
49-
evaluate_product_rules_task.delay(product_uuid=product.uuid)
74+
logger.info(f"Evaluating policy rules for product {product}")
75+
new_violations, resolved_count = evaluate_rules(product)
76+
logger.info(
77+
f"Policy rules evaluated for {product}: "
78+
f"{len(new_violations)} new violation(s), {resolved_count} resolved."
79+
)
80+
fire_policy_webhooks(product, new_violations, resolved_count)
81+
82+
logger.info(f"Policy rule evaluation complete for {count} product(s).")

0 commit comments

Comments
 (0)