Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 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
156 changes: 156 additions & 0 deletions Packs/Pwned/Integrations/PwnedV2/PwnedV2.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,158 @@ def pwned_username(username_list):
return api_res_list


def pwned_breaches_for_domain_list_command(args_dict: dict) -> tuple[list, list, list]:
"""Get all breached email addresses for one or more domains.

API endpoint: GET /breachedDomain/{domain}
The API returns a JSON object where keys are email aliases and values are
lists of breach names, e.g. {"alias1": ["Adobe"], "alias2": ["Adobe", "Gawker"]}.

On HTTP 404 the domain has no breached email addresses.

Args:
args_dict: demisto.args() dictionary. Expected key: ``domain`` (comma-separated list).

Returns:
Tuple of (md_list, ec_list, api_res_list) matching the existing command pattern.
"""
domain_list: list[str] = argToList(args_dict.get("domain", ""))

md_list: list[str] = []
ec_list: list[dict] = []
api_res_list: list = []

for domain in domain_list:
api_res = http_request("GET", f"breachedDomain/{domain}")

if api_res is None:
md_list.append(f"### Breaches for domain: *{domain}*\nThe domain does not have any email addresses")
ec_list.append({})
api_res_list.append(None)
continue

# api_res is a dict {alias: [breach_name, ...], ...}
# Build HR table with columns: Domain, Account (alias), Breaches
table_data = [
{"Domain": domain, "Account": alias, "Breaches": ", ".join(breaches)} for alias, breaches in api_res.items()
]
md = tableToMarkdown(f"Breaches for domain: {domain}", table_data, ["Domain", "Account", "Breaches"])

# Build entry context under "Domain.Pwned-V2.Breaches"
ec: dict = {"Domain.Pwned-V2.Breaches": api_res}

md_list.append(md)
ec_list.append(ec)
api_res_list.append(api_res)

return md_list, ec_list, api_res_list


def pwned_subscribed_domains_list_command(args_dict: dict) -> tuple[list, list, list]:
"""Get the list of subscribed (verified) domains for the API key owner.

API endpoint: GET /subscribedDomains
No input arguments required.

Returns:
Tuple of (md_list, ec_list, api_res_list) matching the existing command pattern.
"""
api_res = http_request("GET", "subscribedDomains")

if api_res is None:
return ["No subscribed domains found."], [{}], [None]

# Build HR table
hr_headers = [
"DomainName",
"PwnCount",
"PwnCountExcludingSpamLists",
"PwnCountExcludingSpamListsAtLastSubscriptionRenewal",
"NextSubscriptionRenewal",
]
md = tableToMarkdown("Subscribed Domains", api_res, hr_headers)

# Build entry context
ec: dict = {"Pwned-V2.SubscribedDomain": api_res}

return [md], [ec], [api_res]


def pwned_latest_breach_get_command(args_dict: dict) -> tuple[list, list, list]:
"""Get the most recently added breach.

API endpoint: GET /latestBreach
No input arguments required.

The response is a single breach object (same schema as domain/pwned-domain commands).
Should reuse ``domain_to_entry_context()`` for context and dBotScore handling.

Returns:
Tuple of (md_list, ec_list, api_res_list) matching the existing command pattern.
"""
api_res = http_request("GET", "latestBreach")

if api_res is None:
return ["No latest breach found."], [{}], [None]

# Build HR table
table_data = [
{
"Latest breach domain name": api_res.get("Domain", ""),
"Breach Date": api_res.get("BreachDate", ""),
"Added Date": api_res.get("AddedDate", ""),
"Pwn Count": api_res.get("PwnCount", ""),
}
]
md = tableToMarkdown("Latest Breach", table_data, ["Latest breach domain name", "Breach Date", "Added Date", "Pwn Count"])

# Build entry context reusing domain_to_entry_context()
domain = api_res.get("Domain", "")
ec = domain_to_entry_context(domain, [api_res])
Comment thread
TheL0L marked this conversation as resolved.

return [md], [ec], [api_res]


def pwned_breach_get_command(args_dict: dict) -> tuple[list, list, list]:
"""Get a single breached site by its breach name.

API endpoint: GET /breach/{breach_name}
The response is a single breach object (same schema as domain/pwned-domain commands).
Should reuse ``domain_to_entry_context()`` for context and dBotScore handling.

Args:
args_dict: demisto.args() dictionary. Expected key: ``breach_name``.

Returns:
Tuple of (md_list, ec_list, api_res_list) matching the existing command pattern.
"""
breach_name: str = args_dict.get("breach_name", "")

api_res = http_request("GET", f"breach/{breach_name}")

if api_res is None:
return [f"No breach found for name: {breach_name}"], [{}], [None]

# Build HR table (same format as latest breach per design doc)
table_data = [
{
"Latest breach domain name": api_res.get("Domain", ""),
"Breach Date": api_res.get("BreachDate", ""),
"Added Date": api_res.get("AddedDate", ""),
"Pwn Count": api_res.get("PwnCount", ""),
}
]
md = tableToMarkdown(
f"Breach: {breach_name}", table_data, ["Latest breach domain name", "Breach Date", "Added Date", "Pwn Count"]
)

# Build entry context reusing domain_to_entry_context()
domain = api_res.get("Domain", "")
ec = domain_to_entry_context(domain, [api_res])
Comment thread
TheL0L marked this conversation as resolved.

return [md], [ec], [api_res]


def main(): # pragma: no cover
if not API_KEY:
raise DemistoException("API key must be provided.")
Expand All @@ -334,6 +486,10 @@ def main(): # pragma: no cover
"domain": pwned_domain_command,
"pwned-domain": pwned_domain_command,
"pwned-username": pwned_username_command,
"pwned-breaches-for-domain-list": pwned_breaches_for_domain_list_command,
"pwned-subscribed-domains-list": pwned_subscribed_domains_list_command,
"pwned-latest-breach-get": pwned_latest_breach_get_command,
"pwned-breach-get": pwned_breach_get_command,
}
if command in commands:
md_list, ec_list, api_email_res_list = commands[command](demisto.args())
Expand Down
111 changes: 110 additions & 1 deletion Packs/Pwned/Integrations/PwnedV2/PwnedV2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,46 @@ configuration:
type: 4
hidden: true
required: false
section: Connect
- displaypassword: API Key
name: credentials_api_key
hiddenusername: true
type: 9
required: false
section: Connect
- defaultvalue: '30'
display: Maximum time per request (in seconds)
name: max_retry_time
type: 0
required: false
section: Connect
- defaultvalue: SUSPICIOUS
display: 'Email Severity: The DBot reputation for compromised emails (SUSPICIOUS or MALICIOUS)'
name: default_dbot_score_email
type: 0
required: false
section: Connect
- defaultvalue: SUSPICIOUS
display: 'Domain Severity: The DBot reputation for compromised domains (SUSPICIOUS or MALICIOUS)'
name: default_dbot_score_domain
type: 0
required: false
section: Connect
- display: Trust any certificate (not secure)
name: insecure
type: 8
required: false
section: Connect
- display: Use system proxy settings
name: proxy
type: 8
required: false
section: Connect
- additionalinfo: Reliability of the source providing the intelligence data.
defaultvalue: A - Completely reliable
display: Source Reliability
name: integrationReliability
section: Connect
options:
- A+ - 3rd party enrichment
- A - Completely reliable
Expand All @@ -61,11 +69,13 @@ configuration:
- suddenDeath
type: 17
required: false
section: Connect
- defaultvalue: '20160'
name: feedExpirationInterval
display: ''
type: 1
required: false
section: Connect
description: Uses the Have I Been Pwned? service to check whether email addresses, domains, or usernames were compromised in previous breaches.
display: Have I Been Pwned? v2
name: Have I Been Pwned? V2
Expand Down Expand Up @@ -239,11 +249,110 @@ script:
- contextPath: Username.Malicious.Description
description: For malicious usernames, the reason that the vendor made the decision.
type: String
- arguments:
- default: true
description: Comma-separated list of domains to check for breaches.
isArray: true
name: domain
required: true
description: Gets all breached email addresses for a domain.
name: pwned-breaches-for-domain-list
outputs:
- contextPath: Domain.Pwned-V2.Breaches
description: A dictionary of breached email aliases and their associated breach names for the domain.
type: Unknown
- arguments: []
description: Gets the list of subscribed domains.
name: pwned-subscribed-domains-list
outputs:
- contextPath: Pwned-V2.SubscribedDomain.DomainName
description: The full domain name that has been successfully verified.
type: String
- contextPath: Pwned-V2.SubscribedDomain.PwnCount
description: Total number of breached email addresses found on the domain at last search.
type: Number
- contextPath: Pwned-V2.SubscribedDomain.PwnCountExcludingSpamLists
description: Number of breached email addresses found on the domain, excluding spam lists.
type: Number
- contextPath: Pwned-V2.SubscribedDomain.PwnCountExcludingSpamListsAtLastSubscriptionRenewal
description: Total breached email addresses found when the current subscription was taken out.
type: Number
- contextPath: Pwned-V2.SubscribedDomain.NextSubscriptionRenewal
description: The date and time the current subscription ends in ISO 8601 format.
type: Date
- arguments: []
description: Gets the most recently added breach.
name: pwned-latest-breach-get
outputs:
- contextPath: Domain.Pwned-V2.Compromised.Vendor
description: For compromised domains, the vendor that made the decision.
type: String
- contextPath: Domain.Pwned-V2.Compromised.Reporters
description: For compromised domains, the reporters for the vendor to make the compromised decision.
type: String
- contextPath: Domain.Name
description: Domain name.
type: String
- contextPath: Domain.Malicious.Vendor
description: For malicious domains, the vendor that made the decision.
type: String
- contextPath: Domain.Malicious.Description
description: For malicious domains, the reason that the vendor made the decision.
type: String
- contextPath: DBotScore.Indicator
description: The indicator that was tested.
type: String
- contextPath: DBotScore.Type
description: The indicator type.
type: String
- contextPath: DBotScore.Vendor
description: The vendor used to calculate the score.
type: String
- contextPath: DBotScore.Score
description: The actual score.
type: Number
- arguments:
- default: true
description: The name of the breach to retrieve.
name: breach_name
required: true
description: Gets a single breached site by breach name.
name: pwned-breach-get
outputs:
- contextPath: Domain.Pwned-V2.Compromised.Vendor
description: For compromised domains, the vendor that made the decision.
type: String
- contextPath: Domain.Pwned-V2.Compromised.Reporters
description: For compromised domains, the reporters for the vendor to make the compromised decision.
type: String
- contextPath: Domain.Name
description: Domain name.
type: String
- contextPath: Domain.Malicious.Vendor
description: For malicious domains, the vendor that made the decision.
type: String
- contextPath: Domain.Malicious.Description
description: For malicious domains, the reason that the vendor made the decision.
type: String
- contextPath: DBotScore.Indicator
description: The indicator that was tested.
type: String
- contextPath: DBotScore.Type
description: The indicator type.
type: String
- contextPath: DBotScore.Vendor
description: The vendor used to calculate the score.
type: String
- contextPath: DBotScore.Score
description: The actual score.
type: Number
runonce: false
script: '-'
subtype: python3
dockerimage: demisto/python3:3.12.8.3296088
dockerimage: demisto/python3:3.12.13.8160132
type: python
tests:
- Pwned v2 test
fromversion: 5.0.0
sectionorder:
- Connect
Loading
Loading