Skip to content

Commit b49ba3e

Browse files
authored
Merge pull request #34 from nightfallai/evan/plat-1628-add-alertconfig-to-sdks
Add support for AlertConfig
2 parents d0b18d9 + 453045b commit b49ba3e

File tree

3 files changed

+95
-7
lines changed

3 files changed

+95
-7
lines changed

nightfall/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
:license: MIT, see LICENSE for more details.
77
"""
88
from .api import Nightfall
9+
from .alerts import SlackAlert, EmailAlert, WebhookAlert, AlertConfig
910
from .detection_rules import (Regex, WordList, Confidence, ContextRule, MatchType, ExclusionRule, MaskConfig,
1011
RedactionConfig, Detector, LogicalOp, DetectionRule)
1112
from .findings import Finding, Range
1213

13-
__all__ = ["Nightfall", "Regex", "WordList", "Confidence", "ContextRule", "MatchType", "ExclusionRule", "MaskConfig",
14-
"RedactionConfig", "Detector", "LogicalOp", "DetectionRule", "Finding", "Range"]
14+
__all__ = ["Nightfall", "SlackAlert", "EmailAlert", "WebhookAlert", "AlertConfig", "Regex", "WordList", "Confidence",
15+
"ContextRule", "MatchType", "ExclusionRule", "MaskConfig", "RedactionConfig", "Detector", "LogicalOp",
16+
"DetectionRule", "Finding", "Range"]

nightfall/alerts.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from dataclasses import dataclass
2+
3+
from typing import List, Tuple, Optional
4+
5+
@dataclass
6+
class SlackAlert:
7+
"""SlackAlert contains the configuration required to allow clients to send asynchronous alerts to a Slack
8+
workspace when findings are detected. Note that in order for Slack alerts to be delivered to your workspace,
9+
you must use authenticate Nightfall to your Slack workspace under the Settings menu on the Nightfall Dashboard.
10+
11+
Currently, Nightfall supports delivering alerts to public channels, formatted like "#general".
12+
Alerts are only sent if findings are detected.
13+
Attributes:
14+
target (str): the channel name, formatted like "#general".
15+
"""
16+
target: str
17+
18+
def as_dict(self):
19+
return {"target": self.target}
20+
21+
@dataclass
22+
class EmailAlert:
23+
"""EmailAlert contains the configuration required to allow clients to send an asynchronous email message
24+
when findings are detected. The findings themselves will be delivered as a file attachment on the email.
25+
Alerts are only sent if findings are detected.
26+
Attributes:
27+
address (str): the email address to which alerts should be sent.
28+
"""
29+
address: str
30+
31+
def as_dict(self):
32+
return {"address": self.address}
33+
34+
@dataclass
35+
class WebhookAlert:
36+
"""WebhookAlert contains the configuration required to allow clients to send a webhook event to an
37+
external URL when findings are detected. The URL provided must (1) use the HTTPS scheme, (2) have a
38+
route defined on the HTTP POST method, and (3) return a 200 status code upon receipt of the event.
39+
40+
In contrast to other platforms, when using the file scanning APIs, an alert is also sent to this webhook
41+
*even when there are no findings*.
42+
Attributes:
43+
address (str): the URL to which alerts should be sent.
44+
"""
45+
address: str
46+
47+
def as_dict(self):
48+
return {"address": self.address}
49+
50+
@dataclass
51+
class AlertConfig:
52+
"""AlertConfig allows clients to specify where alerts should be delivered when findings are discovered as
53+
part of a scan. These alerts are delivered asynchronously to all destinations specified in the object instance.
54+
Attributes:
55+
slack (SlackAlert): Send alerts to a Slack workspace when findings are detected.
56+
email (EmailAlert): Send alerts to an email address when findings are detected.
57+
url (WebhookAlert): Send an HTTP webhook event to a URL when findings are detected.
58+
"""
59+
slack: Optional[SlackAlert] = None
60+
email: Optional[EmailAlert] = None
61+
url: Optional[WebhookAlert] = None
62+
63+
def as_dict(self):
64+
result = {}
65+
if self.slack:
66+
result["slack"] = self.slack.as_dict()
67+
if self.email:
68+
result["email"] = self.email.as_dict()
69+
if self.url:
70+
result["url"] = self.url.as_dict()
71+
return result
72+

nightfall/api.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from requests.adapters import HTTPAdapter
1616
from urllib3 import Retry
1717

18+
from nightfall.alerts import AlertConfig
1819
from nightfall.detection_rules import DetectionRule, RedactionConfig
1920
from nightfall.exceptions import NightfallUserError, NightfallSystemError
2021
from nightfall.findings import Finding
@@ -57,7 +58,7 @@ def __init__(self, key: Optional[str] = None, signing_secret: Optional[str] = No
5758

5859
def scan_text(self, texts: List[str], policy_uuids: List[str] = None, detection_rules: Optional[List[DetectionRule]] = None,
5960
detection_rule_uuids: Optional[List[str]] = None, context_bytes: Optional[int] = None,
60-
default_redaction_config: Optional[RedactionConfig] = None) ->\
61+
default_redaction_config: Optional[RedactionConfig] = None, alert_config: Optional[AlertConfig] = None) ->\
6162
Tuple[List[List[Finding]], List[str]]:
6263
"""Scan text with Nightfall.
6364
@@ -83,6 +84,8 @@ def scan_text(self, texts: List[str], policy_uuids: List[str] = None, detection_
8384
:param default_redaction_config: The default redaction configuration to apply to all detection rules, unless
8485
there is a more specific config within a detector.
8586
:type default_redaction_config: RedactionConfig or None
87+
:param alert_config: Configures external destinations to fan out alerts to in the event that findings are detected.
88+
:type alert_config: AlertConfig or None
8689
:returns: list of findings, list of redacted input texts
8790
"""
8891

@@ -98,6 +101,8 @@ def scan_text(self, texts: List[str], policy_uuids: List[str] = None, detection_
98101
policy["contextBytes"] = context_bytes
99102
if default_redaction_config:
100103
policy["defaultRedactionConfig"] = default_redaction_config.as_dict()
104+
if alert_config:
105+
policy["alertConfig"] = alert_config.as_dict()
101106

102107
request_body = {
103108
"payload": texts
@@ -132,7 +137,8 @@ def _scan_text_v3(self, data: dict):
132137
def scan_file(self, location: str, webhook_url: Optional[str] = None, policy_uuid: Optional[str] = None,
133138
detection_rules: Optional[List[DetectionRule]] = None,
134139
detection_rule_uuids: Optional[List[str]] = None,
135-
request_metadata: Optional[str] = None) -> Tuple[str, str]:
140+
request_metadata: Optional[str] = None,
141+
alert_config: Optional[AlertConfig] = None) -> Tuple[str, str]:
136142
"""Scan file with Nightfall.
137143
At least one of policy_uuid, detection_rule_uuids or detection_rules is required.
138144
@@ -146,6 +152,8 @@ def scan_file(self, location: str, webhook_url: Optional[str] = None, policy_uui
146152
:type detection_rule_uuids: List[str] or None
147153
:param request_metadata: additional metadata that will be returned with the webhook response
148154
:type request_metadata: str or None
155+
:param alert_config: Configures external destinations to fan out alerts to in the event that findings are detected.
156+
:type alert_config: AlertConfig or None
149157
:returns: (scan_id, message)
150158
"""
151159

@@ -169,7 +177,8 @@ def scan_file(self, location: str, webhook_url: Optional[str] = None, policy_uui
169177
detection_rules=detection_rules,
170178
detection_rule_uuids=detection_rule_uuids,
171179
webhook_url=webhook_url, policy_uuid=policy_uuid,
172-
request_metadata=request_metadata)
180+
request_metadata=request_metadata,
181+
alert_config=alert_config)
173182
_validate_response(response, 200)
174183
parsed_response = response.json()
175184

@@ -216,15 +225,20 @@ def _file_scan_finalize(self, session_id: str):
216225

217226
def _file_scan_scan(self, session_id: str, detection_rules: Optional[List[DetectionRule]] = None,
218227
detection_rule_uuids: Optional[List[str]] = None, webhook_url: Optional[str] = None,
219-
policy_uuid: Optional[str] = None, request_metadata: Optional[str] = None) -> requests.Response:
228+
policy_uuid: Optional[str] = None, request_metadata: Optional[str] = None,
229+
alert_config: Optional[AlertConfig] = None) -> requests.Response:
220230
if policy_uuid:
221231
data = {"policyUUID": policy_uuid}
222232
else:
223-
data = {"policy": {"webhookURL": webhook_url}}
233+
data = {"policy": {}}
234+
if webhook_url:
235+
data["policy"]["webhookURL"] = webhook_url
224236
if detection_rule_uuids:
225237
data["policy"]["detectionRuleUUIDs"] = detection_rule_uuids
226238
if detection_rules:
227239
data["policy"]["detectionRules"] = [d.as_dict() for d in detection_rules]
240+
if alert_config:
241+
data["policy"]["alertConfig"] = alert_config.as_dict()
228242

229243
if request_metadata:
230244
data["requestMetadata"] = request_metadata

0 commit comments

Comments
 (0)