diff --git a/Packs/SilentPush/Integrations/SilentPush/README.md b/Packs/SilentPush/Integrations/SilentPush/README.md index 8eac72c21238..5d3a1faccf2c 100644 --- a/Packs/SilentPush/Integrations/SilentPush/README.md +++ b/Packs/SilentPush/Integrations/SilentPush/README.md @@ -444,7 +444,7 @@ This command retrieves comprehensive enrichment information for a given resource | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | resource | Type of resource for which information needs to be retrieved {e.g. domain}. | Required | -| value | Value corresponding to the selected "resource" for which information needs to be retrieved{e.g. silentpush.com}. | Required | +| value | Value corresponding to the selected "resource" for which information needs to be retrieved {e.g. silentpush.com}. | Required | | explain | Include explanation of data calculations. | Optional | | scan_data | Include scan data (IPv4 only). | Optional | @@ -452,7 +452,7 @@ This command retrieves comprehensive enrichment information for a given resource | **Path** | **Type** | **Description** | | --- | --- | --- | -| SilentPush.Enrichment.value | String | Queried value | +| SilentPush.Enrichment.value | String | Queried value. | | SilentPush.Enrichment.domain_string_frequency_probability.avg_probability | Number | Average probability score of the domain string. | | SilentPush.Enrichment.domain_string_frequency_probability.dga_probability_score | Number | Probability score indicating likelihood of being a DGA domain. | | SilentPush.Enrichment.domain_string_frequency_probability.domain | String | Domain name analyzed. | @@ -465,7 +465,7 @@ This command retrieves comprehensive enrichment information for a given resource | SilentPush.Enrichment.domain_urls.results_summary.is_dynamic_domain | Boolean | Indicates if the domain is dynamic. | | SilentPush.Enrichment.domain_urls.results_summary.is_url_shortener | Boolean | Indicates if the domain is a known URL shortener. | | SilentPush.Enrichment.domain_urls.results_summary.results | Number | Number of results found for the domain. | -| SilentPush.Enrichment.domain_urls.results_summary.url_shortner_score | Number | Score of the shortned URL | +| SilentPush.Enrichment.domain_urls.results_summary.url_shortner_score | Number | Score of the shortned URL. | | SilentPush.Enrichment.domaininfo.domain | String | Domain name analyzed. | | SilentPush.Enrichment.domaininfo.error | String | Error message if no data is available for the domain. | | SilentPush.Enrichment.domaininfo.zone | String | TLD zone of the domain. | @@ -490,9 +490,9 @@ This command retrieves comprehensive enrichment information for a given resource | SilentPush.Enrichment.ns_reputation.ns_reputation_score | Number | Reputation score of the domain’s nameservers. | | SilentPush.Enrichment.ns_reputation.ns_srv_reputation.domain | String | The nameservers of domain. | | SilentPush.Enrichment.ns_reputation.ns_srv_reputation.ns_server | String | Provided nameserver. | -| SilentPush.Enrichment.ns_reputation.ns_srv_reputation.ns_server_domain_density | Number | Number of domains sharing this NS | +| SilentPush.Enrichment.ns_reputation.ns_srv_reputation.ns_server_domain_density | Number | Number of domains sharing this NS. | | SilentPush.Enrichment.ns_reputation.ns_srv_reputation.ns_server_domains_listed | Number | Number of listed domains using this NS. | -| SilentPush.Enrichment.ns_reputation.ns_srv_reputation.ns_server_reputation | Number | Reputation score for this NS | +| SilentPush.Enrichment.ns_reputation.ns_srv_reputation.ns_server_reputation | Number | Reputation score for this NS. | | SilentPush.Enrichment.scan_data.certificates.domain | String | Domain for which the SSL certificate was issued. | | SilentPush.Enrichment.scan_data.certificates.domains | Unknown | Other Domains for which the SSL certificate was issued. | | SilentPush.Enrichment.scan_data.certificates.issuer_organization | String | Issuer organization of the SSL certificate. | @@ -508,13 +508,13 @@ This command retrieves comprehensive enrichment information for a given resource | SilentPush.Enrichment.scan_data.headers.hostname | String | The hostname that sent this response. | | SilentPush.Enrichment.scan_data.headers.ip | String | The IP address responding to the request. | | SilentPush.Enrichment.scan_data.headers.scan_date | String | The date when the headers were scanned. | -| SilentPush.Enrichment.scan_data.headers.headers.cache-control | String | HTTP cache-control | +| SilentPush.Enrichment.scan_data.headers.headers.cache-control | String | HTTP cache-control. | | SilentPush.Enrichment.scan_data.headers.headers.content-length" | String | Content lenght of the HTTP response. | | SilentPush.Enrichment.scan_data.headers.headers.date | String | The date/time of the response. | | SilentPush.Enrichment.scan_data.headers.headers.expires | String | Indicates an already expired response. | | SilentPush.Enrichment.scan_data.headers.headers.server | String | The web server handling the request \(Cloudflare proxy\). | | SilentPush.Enrichment.scan_data.html.hostname | String | HTTP response code for the domain scan. | -| SilentPush.Enrichment.scan_data.html.html_body_murmur3 | String | hash of the page content | +| SilentPush.Enrichment.scan_data.html.html_body_murmur3 | String | hash of the page content. | | SilentPush.Enrichment.scan_data.html.html_body_ssdeep | String | SSDEEP hash \(used for fuzzy matching similar HTML content\). | | SilentPush.Enrichment.scan_data.html.html_title | String | The page title \(suggests a Cloudflare challenge page, likely due to bot protection\). | | SilentPush.Enrichment.scan_data.html.ip | String | The IP address responding to the request. | @@ -614,7 +614,7 @@ This command retrieves comprehensive enrichment information for a given resource ### **Command Example** ```bash -!silentpush-get-enrichment-data resource="ipv4" value="142.251.188.102" +!silentpush-get-enrichment-data resource="ipv4" value="0.0.0.0" ``` ### **Context Example** @@ -622,7 +622,7 @@ This command retrieves comprehensive enrichment information for a given resource ```json { "resource": "ipv4", - "value": "142.251.188.102", + "value": "0.0.0.0", "enrichment_data": { "asn": "15169", "asn_allocation_age": 9140, @@ -641,7 +641,7 @@ This command retrieves comprehensive enrichment information for a given resource "tags": [], "date": "2025-04-08", "density": 0, - "ip": "142.251.188.102", + "ip": "0.0.0.0", "ip_flags": { "is_proxy": false, "is_sinkhole": false, @@ -1296,14 +1296,14 @@ This command get IP information for multiple IPv4s and IPv6s. ### **Command Example** ```bash -!silentpush-list-ip-information ips="142.251.188.102" +!silentpush-list-ip-information ips="0.0.0.0" ``` ### **Context Example** ```json { - "ips": ["142.251.188.102"], + "ips": ["0.0.0.0"], "ip_information": { "asn": "15169", "asn_allocation_age": 9140, @@ -1323,7 +1323,7 @@ This command get IP information for multiple IPv4s and IPv6s. "tags": [], "date": "2025-04-08", "density": 0, - "ip": "142.251.188.102", + "ip": "0.0.0.0", "ip_flags": { "is_proxy": false, "is_sinkhole": false, @@ -1483,17 +1483,17 @@ This command scan a URL to retrieve hosting metadata.. ### **Command Example** ```bash -!silentpush-live-url-scan url="https://silentpush.com" +!silentpush-live-url-scan url="https://example.com" ``` ### **Context Example** ```json { - "url": "https://silentpush.com", + "url": "https://example.com", "scan_results": { "status": "No scan results found", - "url": "https://silentpush.com" + "url": "https://example.com" } } ``` @@ -1504,7 +1504,7 @@ This command scan a URL to retrieve hosting metadata.. > >| Field | Value | >|----------------|----------------------------| ->| URL | | +>| URL | | >| Scan Status | No scan results found | ### silentpush-reverse-padns-lookup @@ -1816,7 +1816,7 @@ This command search Silent Push scan data repositories using SPQL queries. "scan_data": [ { "domain": "volunteering.cool", - "ip": "44.227.65.245", + "ip": "0.0.0.0", "asn": "16509", "asn_org": "AMAZON-02", "city": "Boardman", @@ -1827,8 +1827,8 @@ This command search Silent Push scan data repositories using SPQL queries. "timezone": "America/Los_Angeles", "server": "openresty", "ssl": "http", - "favicon": "http://volunteering.cool/favicon.ico", - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.3", + "favicon": "http://example.cool/favicon.ico", + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/0.0.0.0 Safari/537.3", "scan_date": "2025-04-08T09:12:45Z", "status_code": 200 } @@ -1842,8 +1842,8 @@ This command search Silent Push scan data repositories using SPQL queries. > >| Field | Value | >|-------------------------|--------------------------------------------| ->| Domain | [volunteering.cool](http://volunteering.cool) | ->| IP Address | 44.227.65.245 | +>| Domain | [volunteering.cool](http://example.cool) | +>| IP Address | 0.0.0.0 | >| ASN | 16509 | >| ASN Organization | AMAZON-02 | >| City | Boardman | @@ -1854,7 +1854,161 @@ This command search Silent Push scan data repositories using SPQL queries. >| Timezone | America/Los_Angeles | >| Server | openresty | >| SSL/TLS Status | HTTP (No SSL) | ->| Favicon | ![Favicon](http://volunteering.cool/favicon.ico) | +>| Favicon | ![Favicon](http://example.cool/favicon.ico) | >| User Agent | Mozilla/5.0 (Linux x86_64) | >| Scan Date | 2025-04-08T09:12:45Z | >| HTTP Status Code | 200 | + +### silentpush-run-threat-check + +*** +This command runs the threat check on the specified + +#### Base Command + +`silentpush-run-threat-check` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| data | The name of the data source to query. | Required | +| query | The value to check for threats (e.g., IP or domain). | Required | +| type | The type of the value being queried (e.g., ip, domain). | Required | +| user_identifier | A unique identifier for the user making the request. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SilentPush.RunThreatCheck.is_listed | Boolean | Indicates whether the queried value is listed as a threat. | +| SilentPush.RunThreatCheck.listed_txt | String | Textual description of the listing status. | +| SilentPush.RunThreatCheck.query | String | The original value that was checked. | + +### silentpush-add-indicators + +*** +This command add indicators to the feed + +#### Base Command + +`silentpush-add-indicators` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| feed_uuid | UUID of the feed. | Required | +| indicators | Indicators for the feed. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SilentPush.AddIndicators.created_or_updated | Unknown | List of indicator names that were created or updated in the feed. | +| SilentPush.AddIndicators.invalid_indicators | Unknown | List of indicators that were considered invalid and not added to the feed. | + +### silentpush-add-feed + +*** +This command add the new feed + +#### Base Command + +`silentpush-add-feed` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| name | Name of the feed. | Required | +| type | Feed Type. | Required | +| category | Feed Category. | Optional | +| vendor | Vendor. | Optional | +| feed_description | URL for the screenshot. | Optional | +| tags | Tags that should be attached with the feed. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SilentPush.Feed.name | String | The name of the feed. | +| SilentPush.Feed.type | String | The type of the feed. | +| SilentPush.Feed.vendor | String | The vendor of the feed. | +| SilentPush.Feed.feed_description | String | A description of the feed. | +| SilentPush.Feed.category | String | The category of the feed. | +| SilentPush.Feed.tags | Unknown | Tags associated with the feed. | + +### silentpush-add-feed-tags + +*** +This command add indicators to the feed + +#### Base Command + +`silentpush-add-feed-tags` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| feed_uuid | Never return query metadata, even if original request did include metadata. | Optional | +| tags | Comma separated tags to be updated to the feed. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SilentPush.AddFeedTags.created_or_updated | Unknown | List of indicator names that were created or updated in the feed. | +| SilentPush.AddFeedTags.invalid_indicators | Unknown | List of indicators that were considered invalid and not added to the feed. | + +### silentpush-get-data-exports + +*** +This command runs the threat check on the specified + +#### Base Command + +`silentpush-get-data-exports` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| feed_url | The URL from which to export the feed data. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SilentPush.GetDataExports.EntryID | Unknown | The EntryID of the report file. | +| SilentPush.GetDataExports.Extension | String | The extension of the report file. | +| SilentPush.GetDataExports.Name | String | The name of the report file. | +| SilentPush.GetDataExports.Info | String | The info of the report file. | +| SilentPush.GetDataExports.Size | Number | The size of the report file. | +| SilentPush.GetDataExports.Type | String | The type of the report file. | + +### silentpush-add-indicator-tags + +*** +This command updates tags to the indicators + +#### Base Command + +`silentpush-add-indicator-tags` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| feed_uuid | UUID of the feed. | Required | +| indicator_name | The name of the indicator to tag. | Required | +| tags | Tags to be added to the indicator. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SilentPush.AddIndicatorTags.uuid | String | The UUID of the indicator. | +| SilentPush.AddIndicatorTags.name | String | The name of the indicator. | +| SilentPush.AddIndicatorTags.tags | String | The tags assigned to the indicator. | diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index b8ccd7908c9e..6befa1f2d3c8 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -44,6 +44,9 @@ LIVE_SCAN_URL = "explore/tools/scanondemand" FUTURE_ATTACK_INDICATOR = "/api/v2/iocs/threat-ranking" SCREENSHOT_URL = "explore/tools/screenshotondemand" +ADD_FEED = "api/v1/feeds/" +THREAT_CHECK = "https://api.threatcheck.silentpush.com/v1/" +EXPORT_DATA = "app/v1/export/organization-exports/" """ COMMANDS INPUTS """ @@ -65,9 +68,9 @@ ), ] NAMESERVER_REPUTATION_INPUTS = [ - InputArgument(name="nameserver", description="Nameserver name for which information needs to be retrieved", required=True), - InputArgument(name="explain", description="Show the information used to calculate the reputation score"), - InputArgument(name="limit", description="The maximum number of reputation history to retrieve"), + InputArgument(name="nameserver", description="Nameserver name for which information needs to be retrieved.", required=True), + InputArgument(name="explain", description="Show the information used to calculate the reputation score."), + InputArgument(name="limit", description="The maximum number of reputation history to retrieve."), ] SUBNET_REPUTATION_INPUTS = [ InputArgument( @@ -116,7 +119,7 @@ description="Build infratags from padns data where the as_of timestamp equivalent is between the first_seen " "and the last_seen timestamp - automatically sets mode to padns. Example :- date: yyyy-mm-dd (2021-07-09) - " "fixed date, epoch: number (1625834953) - fixed time in epoch format, sec: negative number (-172800) - " - "relative time seconds ago", + "relative time seconds ago.", default="self", ), ] @@ -176,15 +179,15 @@ IPV4_REPUTATION_INPUTS = [ InputArgument( name="ipv4", # option 1 - description="IPv4 address for which information needs to be retrieved", + description="IPv4 address for which information needs to be retrieved.", required=True, ), - InputArgument(name="explain", description="Show the information used to calculate the reputation score"), - InputArgument(name="limit", description="The maximum number of reputation history to retrieve"), + InputArgument(name="explain", description="Show the information used to calculate the reputation score."), + InputArgument(name="limit", description="The maximum number of reputation history to retrieve."), ] FORWARD_PADNS_INPUTS = [ - InputArgument(name="qtype", description="DNS record type", required=True), - InputArgument(name="qname", description="The DNS record name to lookup", required=True), + InputArgument(name="qtype", description="DNS record type.", required=True), + InputArgument(name="qname", description="The DNS record name to lookup.", required=True), InputArgument(name="netmask", description="The netmask to filter the lookup results."), InputArgument(name="subdomains", description="Flag to include subdomains in the lookup results."), InputArgument(name="regex", description="Regular expression to filter the DNS records."), @@ -249,6 +252,38 @@ ) ] +ADD_FEED_INPUTS = [ + InputArgument(name="name", description="Name of the feed.", required=True), + InputArgument(name="type", description="Feed Type.", required=True), + InputArgument(name="category", description="Feed Category.", required=False), + InputArgument(name="vendor", description="Vendor.", required=False), + InputArgument(name="feed_description", description="URL for the screenshot.", required=False), + InputArgument(name="tags", description="Tags that should be attached with the feed.", required=False), +] + +ADD_INDICATORS_INPUTS = [ + InputArgument(name="feed_uuid", description="UUID of the feed.", required=True), + InputArgument(name="indicators", description="Indicators for the feed.", required=True), +] + +ADD_INDICATOR_TAGS_INPUTS = [ + InputArgument(name="feed_uuid", description="UUID of the feed.", required=True), + InputArgument(name="indicator_name", description="The name of the indicator to tag.", required=True), + InputArgument(name="tags", description="Tags to be added to the indicator.", required=True), +] + +RUN_THREAT_CHECK_INPUTS = [ + InputArgument(name="data", description="The name of the data source to query.", required=True), + InputArgument(name="query", description="The value to check for threats (e.g., IP or domain).", required=True), + InputArgument(name="type", description="The type of the value being queried (e.g., ip, domain).", required=True), + InputArgument(name="user_identifier", description="A unique identifier for the user making the request.", required=True), +] + +GET_DATA_EXPORTS_INPUTS = [ + InputArgument(name="feed_url", description="The URL from which to export the feed data.", required=True) +] + + """ COMMANDS OUTPUTS """ JOB_STATUS_OUTPUTS = [ @@ -414,7 +449,7 @@ ] DOMAIN_CERTIFICATE_OUTPUTS = [ OutputArgument(name="domain", output_type=str, description="Queried domain."), - OutputArgument(name="metadata", output_type=str, description="Metadata of the response"), + OutputArgument(name="metadata", output_type=str, description="Metadata of the response."), OutputArgument(name="certificates.cert_index", output_type=int, description="Index of the certificate."), OutputArgument(name="certificates.chain", output_type=list, description="Certificate chain."), OutputArgument(name="certificates.date", output_type=int, description="Certificate issue date."), @@ -448,7 +483,7 @@ OutputArgument(name="job_details.status", output_type=str, description="Status of the job."), ] ENRICHMENT_OUTPUTS = [ - OutputArgument(name="value", output_type=str, description="Queried value"), + OutputArgument(name="value", output_type=str, description="Queried value."), OutputArgument( name="domain_string_frequency_probability.avg_probability", output_type=float, @@ -475,7 +510,7 @@ OutputArgument( name="domain_urls.results_summary.alexa_top10k_score", output_type=int, - description={repr("Score indicating domain's Alexa top 10k ranking.")}, + description="Score indicating domain's Alexa top 10k ranking.", ), OutputArgument( name="domain_urls.results_summary.dynamic_domain_score", @@ -498,7 +533,7 @@ description="Number of results found for the domain.", ), OutputArgument( - name="domain_urls.results_summary.url_shortner_score", output_type=int, description="Score of the shortned URL" + name="domain_urls.results_summary.url_shortner_score", output_type=int, description="Score of the shortned URL." ), OutputArgument(name="domaininfo.domain", output_type=str, description="Domain name analyzed."), OutputArgument(name="domaininfo.error", output_type=str, description="Error message if no data is available for the domain."), @@ -573,7 +608,7 @@ OutputArgument( name="ns_reputation.ns_srv_reputation.ns_server_domain_density", output_type=int, - description="Number of domains sharing this NS", + description="Number of domains sharing this NS.", ), OutputArgument( name="ns_reputation.ns_srv_reputation.ns_server_domains_listed", @@ -583,7 +618,7 @@ OutputArgument( name="ns_reputation.ns_srv_reputation.ns_server_reputation", output_type=int, - description="Reputation score for this NS", + description="Reputation score for this NS.", ), OutputArgument( name="scan_data.certificates.domain", @@ -638,7 +673,7 @@ OutputArgument(name="scan_data.headers.hostname", output_type=str, description="The hostname that sent this response."), OutputArgument(name="scan_data.headers.ip", output_type=str, description="The IP address responding to the request."), OutputArgument(name="scan_data.headers.scan_date", output_type=str, description="The date when the headers were scanned."), - OutputArgument(name="scan_data.headers.headers.cache-control", output_type=str, description="HTTP cache-control"), + OutputArgument(name="scan_data.headers.headers.cache-control", output_type=str, description="HTTP cache-control."), OutputArgument( name='scan_data.headers.headers.content-length"', output_type=str, @@ -654,7 +689,7 @@ description="The web server handling the request (Cloudflare proxy).", ), OutputArgument(name="scan_data.html.hostname", output_type=str, description="HTTP response code for the domain scan."), - OutputArgument(name="scan_data.html.html_body_murmur3", output_type=str, description="hash of the page content"), + OutputArgument(name="scan_data.html.html_body_murmur3", output_type=str, description="hash of the page content."), OutputArgument( name="scan_data.html.html_body_ssdeep", output_type=str, @@ -747,7 +782,7 @@ name="ip2asn.benign_info.known_benign", output_type=bool, description="Indicates whether this IP/ASN is explicitly known to be safe " - "(e.g., a reputable cloud provider or public service)", + "(e.g., a reputable cloud provider or public service).", ), OutputArgument( name="ip2asn.benign_info.tags", @@ -1737,6 +1772,41 @@ OutputArgument(name="url", output_type=str, description="The URL that was used to generate the screenshot."), ] +ADD_FEED_OUTPUTS = [ + OutputArgument(name="name", output_type=str, description="The name of the feed."), + OutputArgument(name="type", output_type=str, description="The type of the feed."), + OutputArgument(name="vendor", output_type=str, description="The vendor of the feed."), + OutputArgument(name="feed_description", output_type=str, description="A description of the feed."), + OutputArgument(name="category", output_type=str, description="The category of the feed."), + OutputArgument(name="tags", output_type=list, description="Tags associated with the feed."), +] + + +ADD_INDICATORS_OUTPUTS = [ + OutputArgument( + name="created_or_updated", + output_type=list, + description="List of indicator names that were created or updated in the feed.", + ), + OutputArgument( + name="invalid_indicators", + output_type=list, + description="List of indicators that were considered invalid and not added to the feed.", + ), +] + +ADD_INDICATOR_TAGS_OUTPUTS = [ + OutputArgument(name="uuid", output_type=str, description="The UUID of the indicator."), + OutputArgument(name="name", output_type=str, description="The name of the indicator."), + OutputArgument(name="tags", output_type=str, description="The tags assigned to the indicator."), +] + +RUN_THREAT_CHECK_OUTPUTS = [ + OutputArgument(name="is_listed", output_type=bool, description="Indicates whether the queried value is listed as a threat."), + OutputArgument(name="listed_txt", output_type=str, description="Textual description of the listing status."), + OutputArgument(name="query", output_type=str, description="The original value that was checked."), +] + metadata_collector = YMLMetadataCollector( integration_name="SilentPush", @@ -1832,17 +1902,25 @@ def _http_request( # type: ignore[override] try: response = requests.request( method=method, - url=full_url, # <<< this must be full_url, not something else + url=full_url, headers=self._headers, verify=self.verify, params=params, json=data, proxies=self.proxies, ) - response.raise_for_status() - return response.json() except requests.exceptions.RequestException as e: - raise DemistoException(f"Request error: {str(e)}") + raise DemistoException(f"Connection error: {str(e)}") + + # Check for non-2xx HTTP responses + if not response.ok: + raise DemistoException(f"HTTP {response.status_code} Error: {response.text}", res=response) + + # Try parsing JSON + try: + return response.json() + except ValueError: + raise DemistoException("Failed to parse JSON response.", res=response) def get_job_status(self, job_id: str, params: dict) -> dict[str, Any]: """ @@ -2467,6 +2545,152 @@ def screenshot_url(self, url: str) -> dict[str, Any]: return {"status_code": screenshot_data.get("response", 200), "screenshot_url": screenshot_url} + def add_feed(self, args: dict) -> dict[str, Any]: + """ + Add new feed on SilentPush. + + Args: + args: Payload for filtering and pagination. + + Returns: + Dict[str, Any]: Response containing feed information. + """ + url = self._base_url.replace("/api/v1/merge-api", "") + f"{ADD_FEED}" + + payload = { + "name": args.get("name"), + "type": args.get("type"), + "vendor": args.get("vendor"), + "feed_description": args.get("feed_description"), + "category": args.get("category"), + "tags": argToList(args.get("tags")), + } + remove_nulls_from_dictionary(payload) + + response = self._http_request(method="POST", url=url, data=payload) + + if isinstance(response, dict) and response.get("errors"): + return {"error": f"Failed to add new feed: {response['errors']}"} + + return response + + def add_feed_tags(self, args: dict) -> dict[str, Any]: + """ + Add new feed on SilentPush. + + Args: + args: Payload for filtering and pagination. + + Returns: + Dict[str, Any]: Response containing feed tags information. + """ + feed_uuid = args.get("feed_uuid") + url = self._base_url.replace("/api/v1/merge-api", "") + f"{ADD_FEED}" + f"{feed_uuid}" + "/tags/" + tags = argToList(args.get("tags")) + payload = {"tags": tags} + remove_nulls_from_dictionary(payload) + response = self._http_request(method="POST", url=url, data=payload) + + if isinstance(response, dict) and response.get("errors"): + return {"error": f"Failed to add feed tags: {response['errors']}"} + + return response + + def add_indicators(self, args: dict) -> dict[str, Any]: + """ + Add new indicator on SilentPush. + + Args: + args: Payload for filtering and pagination. + + Returns: + Dict[str, Any]: Response containing indicators information. + """ + feed_uuid = args.get("feed_uuid") + url = self._base_url.replace("/api/v1/merge-api", "") + f"{ADD_FEED}" + f"{feed_uuid}" + "/indicators/" + indicators = argToList(args.get("indicators")) + payload = {"indicators": indicators} + remove_nulls_from_dictionary(payload) + response = self._http_request(method="POST", url=url, data=payload) + + if isinstance(response, dict) and response.get("errors"): + return {"error": f"Failed to add new indicators: {response['errors']}"} + + return response + + def add_indicators_tags(self, args: dict) -> dict[str, Any]: + """ + Add new indicator tags on SilentPush. + + Args: + args: Payload for tags. + + Returns: + Dict[str, Any]: Response containing indicator tags information. + """ + feed_uuid = args.get("feed_uuid") + indicator_name = args.get("indicator_name") + url = ( + self._base_url.replace("/api/v1/merge-api", "") + + f"{ADD_FEED}" + + f"{feed_uuid}" + + "/indicators/" + + f"{indicator_name}" + + "/update-tags/" + ) + tags = argToList(args.get("tags")) + payload = {"tags": tags} + remove_nulls_from_dictionary(payload) + response = self._http_request(method="PUT", url=url, data=payload) + + if isinstance(response, dict) and response.get("errors"): + return {"error": f"Failed to add indicator tags: {response['errors']}"} + + return response + + def run_threat_check(self, args: dict) -> dict[str, Any]: + """ + Fetch threat checks on SilentPush. + + Args: + args: Params for threat checks. + + Returns: + Dict[str, Any]: Response containing threat check information. + """ + url = f"{THREAT_CHECK}" + params = {"t": args.get("type"), "d": args.get("data"), "u": args.get("user_identifier"), "q": args.get("query")} + remove_nulls_from_dictionary(params) + response = self._http_request(method="GET", url=url, params=params) + + if isinstance(response, dict) and response.get("errors"): + return {"error": f"Failed to run threat check: {response['errors']}"} + + return response + + def get_data_exports(self, feed_url: str) -> requests.Response: + """ + Exports data on SilentPush. + + Args: + feed_url: Feed url for exporting data. + + Returns: + Dict[str, Any]: Response containing feed information. + """ + server_url = self._base_url.replace("/api/v1/merge-api", "") + url = f"{server_url}{EXPORT_DATA}{feed_url}" + + response = requests.request( + method="GET", + url=url, # <<< this must be full_url, not something else + headers=self._headers, + verify=self.verify, + proxies=self.proxies, + ) + + return response + """ COMMAND FUNCTIONS """ @@ -2619,7 +2843,7 @@ def get_nameserver_reputation_command(client: Client, args: dict) -> CommandResu @metadata_collector.command( command_name="silentpush-get-subnet-reputation", inputs_list=SUBNET_REPUTATION_INPUTS, - outputs_prefix="SilentPush.NameserverReputation", + outputs_prefix="SilentPush.SubnetReputation", outputs_list=SUBNET_REPUTATION_OUTPUTS, description="This command retrieves the reputation history for a specific subnet.", ) @@ -3912,6 +4136,212 @@ def screenshot_url_command(client: Client, args: dict[str, Any]) -> CommandResul ) +@metadata_collector.command( + command_name="silentpush-add-feed", + inputs_list=ADD_FEED_INPUTS, + outputs_prefix="SilentPush.Feed", + outputs_list=ADD_FEED_OUTPUTS, + description="This command add the new feed", +) +def add_feed_command(client: Client, args: dict[str, Any]) -> CommandResults | dict: + """ + Command handler for adding new feeds. + + Args: + client (Client): SilentPush API client instance. + args (Dict[str, Any]): Command arguments, must include 'name' and 'type' key. + + Returns: + CommandResults: JSON response of added feed. + """ + result = client.add_feed(args) + feed_name = result.get("name") + feed_type = result.get("type") + + return CommandResults( + outputs_prefix="SilentPush.Feed", + outputs_key_field="name", + outputs=result, + readable_output=f"SilentPush feed: {feed_name} of type: {feed_type} was added successfully.", + raw_response=result, + ) + + +ADD_FEED_TAGS_INPUTS = [ + InputArgument( + name="feed_uuid", + description="Never return query metadata, even if original request did include metadata.", + ), + InputArgument( + name="tags", + description="Comma separated tags to be updated to the feed.", + ), +] + +ADD_FEED_TAGS_OUTPUTS = [ + OutputArgument(name="created_or_updated", description="List of tags that have been created or updated to the feed.") +] + + +@metadata_collector.command( + command_name="silentpush-add-feed-tags", + inputs_list=ADD_FEED_TAGS_INPUTS, + outputs_prefix="SilentPush.AddFeedTags", + outputs_list=ADD_INDICATORS_OUTPUTS, + description="This command add indicators to the feed", +) +def add_feed_tags_command(client: Client, args: dict[str, Any]) -> CommandResults | dict: + """ + Command handler for adding new feed tags. + + Args: + client (Client): SilentPush API client instance. + args (Dict[str, Any]): Command arguments, must include 'feed_uuid' key. + + Returns: + CommandResults: JSON response of added tags. + """ + result = client.add_feed_tags(args).get('created_or_updated') + uuid = args.get('feed_uuid') + tags = args.get('tags') + + return CommandResults( + outputs_prefix="SilentPush.AddFeedTags", + outputs_key_field="feed_uuid", + outputs=result, + readable_output=f"feed with uuid: {uuid} was updated with tags: {tags}", + raw_response=result, + ) + + +@metadata_collector.command( + command_name="silentpush-add-indicators", + inputs_list=ADD_INDICATORS_INPUTS, + outputs_prefix="SilentPush.AddIndicators", + outputs_list=ADD_INDICATORS_OUTPUTS, + description="This command add indicators to the feed", +) +def add_indicators_command(client: Client, args: dict[str, Any]) -> CommandResults | dict: + """ + Command handler for add new indicators. + + Args: + client (Client): SilentPush API client instance. + args (Dict[str, Any]): Command arguments, must include 'feed_uuid' and 'indicators key. + + Returns: + CommandResults: JSON response of added indicators. + """ + result = client.add_indicators(args).get('created_or_updated') + indicators = args.get('indicators') + feed_uuid = args.get('feed_uuid') + + return CommandResults( + outputs_prefix="SilentPush.AddIndicators", + outputs_key_field="feed_uuid", + outputs=result, + readable_output=f"Indicators: '{indicators}' were added successfully to SilentPush feed: '{feed_uuid}'.", + raw_response=result, + ) + + +@metadata_collector.command( + command_name="silentpush-add-indicator-tags", + inputs_list=ADD_INDICATOR_TAGS_INPUTS, + outputs_prefix="SilentPush.AddIndicatorTags", + outputs_list=ADD_INDICATOR_TAGS_OUTPUTS, + description="This command updates tags to the indicators", +) +def add_indicators_tags_command(client: Client, args: dict[str, Any]) -> CommandResults | dict: + """ + Command handler for add new indicator tags. + + Args: + client (Client): SilentPush API client instance. + args (Dict[str, Any]): Command arguments, must include 'feed_uuid' and 'indicator_name key. + + Returns: + CommandResults: JSON response of added indicator tags. + """ + result = client.add_indicators_tags(args) + tags = args.get('tags') + indicator_name = args.get('indicator_name') + + return CommandResults( + outputs_prefix="SilentPush.AddIndicatorTags", + outputs_key_field="feed_uuid", + outputs=result, + readable_output=f"Indicator Tags: '{tags}' added to indicator '{indicator_name}' successfully", + raw_response=result, + ) + + +@metadata_collector.command( + command_name="silentpush-run-threat-check", + inputs_list=RUN_THREAT_CHECK_INPUTS, + outputs_prefix="SilentPush.RunThreatCheck", + outputs_list=RUN_THREAT_CHECK_OUTPUTS, + description="This command runs the threat check on the specified ", +) +def run_threat_check_command(client: Client, args: dict[str, Any]) -> CommandResults | dict: + """ + Command handler to fetch threat checks. + + Args: + client (Client): SilentPush API client instance. + args (Dict[str, Any]): Command arguments, must include 'feed_uuid' key. + + Returns: + CommandResults: JSON response of threat check. + """ + result = client.run_threat_check(args) + ip = result.get("query") + + return CommandResults( + outputs_prefix="SilentPush.RunThreatCheck", + outputs_key_field="query", + outputs=result, + readable_output=f"Threat check for query '{ip}' completed successfully", + raw_response=result, + ) + + +@metadata_collector.command( + command_name="silentpush-get-data-exports", + inputs_list=GET_DATA_EXPORTS_INPUTS, + outputs_prefix="SilentPush.GetDataExports", + outputs_list=[], + file_output=True, + description="This command runs the threat check on the specified ", +) +def get_data_exports_command(client: Client, args: dict[str, str]) -> dict[str, Any]: + """ + Command handler to export data. + + Args: + client (Client): SilentPush API client instance. + args (Dict[str, str]): Command arguments, must include 'feed_uuid' key. + + Returns: + CommandResults: JSON response of threat check. + """ + + feed_url = args["feed_url"] + response = client.get_data_exports(feed_url) + + # Check for errors + if response.status_code != 200: + raise Exception(f"Failed to download file: {response.status_code} {response.text}") + + # Choose a filename (you can extract from headers or use feed_url) + filename = feed_url.split("/")[-1] or "exported_data" + + # Return fileResult + file_entry = fileResult(filename, response.content, file_type=EntryType.ENTRY_INFO_FILE) + return file_entry + + + """ MAIN FUNCTION """ @@ -3991,6 +4421,24 @@ def main() -> None: elif demisto.command() == "silentpush-screenshot-url": return_results(screenshot_url_command(client, demisto.args())) + elif demisto.command() == "silentpush-add-feed": + return_results(add_feed_command(client, demisto.args())) + + elif demisto.command() == "silentpush-add-feed-tags": + return_results(add_feed_tags_command(client, demisto.args())) + + elif demisto.command() == "silentpush-add-indicators": + return_results(add_indicators_command(client, demisto.args())) + + elif demisto.command() == "silentpush-add-indicator-tags": + return_results(add_indicators_tags_command(client, demisto.args())) + + elif demisto.command() == "silentpush-run-threat-check": + return_results(run_threat_check_command(client, demisto.args())) + + elif demisto.command() == "silentpush-get-data-exports": + return_results(get_data_exports_command(client, demisto.args())) + except Exception as e: demisto.error(traceback.format_exc()) # print the traceback return_error(f"Failed to execute {demisto.command()} command.\nError:\n{str(e)}") diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index 11c8802f3fdc..82862630dc94 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -3,9 +3,6 @@ description: The Silent Push Platform uses first-party data and a proprietary sc commonfields: id: SilentPush version: -1 -sectionOrder: -- Connect -- Collect name: SilentPush display: SilentPush configuration: @@ -34,6 +31,146 @@ configuration: section: Connect script: commands: + - deprecated: false + description: This command add the new feed + name: silentpush-add-feed + arguments: + - name: name + isArray: false + description: Name of the feed. + required: true + secret: false + default: false + - auto: PREDEFINED + default: false + description: Feed Type. + name: type + required: true + predefined: + - domain + - ip + - URL + - name: category + isArray: false + description: Feed Category. + required: false + secret: false + default: false + - name: vendor + isArray: false + description: Vendor. + required: false + secret: false + default: false + - name: feed_description + isArray: false + description: URL for the screenshot. + required: false + secret: false + default: false + - name: tags + isArray: false + description: Tags that should be attached with the feed. + required: false + secret: false + default: false + outputs: + - contextPath: SilentPush.Feed.name + description: The name of the feed. + type: String + - contextPath: SilentPush.Feed.type + description: The type of the feed. + type: String + - contextPath: SilentPush.Feed.vendor + description: The vendor of the feed. + type: String + - contextPath: SilentPush.Feed.feed_description + description: A description of the feed. + type: String + - contextPath: SilentPush.Feed.category + description: The category of the feed. + type: String + - contextPath: SilentPush.Feed.tags + description: Tags associated with the feed. + type: Unknown + - deprecated: false + description: This command add indicators to the feed + name: silentpush-add-feed-tags + arguments: + - name: feed_uuid + isArray: false + description: Never return query metadata, even if original request did include metadata. + required: true + secret: false + default: false + - name: tags + isArray: false + description: Comma separated tags to be updated to the feed. + required: true + secret: false + default: false + outputs: + - contextPath: SilentPush.AddFeedTags.created_or_updated + description: List of indicator names that were created or updated in the feed. + type: Unknown + - contextPath: SilentPush.AddFeedTags.invalid_indicators + description: List of indicators that were considered invalid and not added to the feed. + type: Unknown + - deprecated: false + description: This command add indicators to the feed + name: silentpush-add-indicators + arguments: + - name: feed_uuid + isArray: false + description: UUID of the feed. + required: true + secret: false + default: false + - name: indicators + isArray: false + description: Indicators for the feed. + required: true + secret: false + default: false + outputs: + - contextPath: SilentPush.AddIndicators.created_or_updated + description: List of indicator names that were created or updated in the feed. + type: Unknown + - contextPath: SilentPush.AddIndicators.invalid_indicators + description: List of indicators that were considered invalid and not added to the feed. + type: Unknown + - deprecated: false + description: This command updates tags to the indicators + name: silentpush-add-indicator-tags + arguments: + - name: feed_uuid + isArray: false + description: UUID of the feed. + required: true + secret: false + default: false + - name: indicator_name + isArray: false + description: The name of the indicator to tag. + required: true + secret: false + default: false + - name: tags + isArray: false + description: Tags to be added to the indicator. + required: true + secret: false + default: false + outputs: + - contextPath: SilentPush.AddIndicatorTags.uuid + description: The UUID of the indicator. + type: String + - contextPath: SilentPush.AddIndicatorTags.name + description: The name of the indicator. + type: String + - contextPath: SilentPush.AddIndicatorTags.tags + description: The tags assigned to the indicator. + type: String - deprecated: false description: This command queries granular DNS/IP parameters (e.g., NS servers, MX servers, IPaddresses, ASNs) for density information. name: silentpush-density-lookup @@ -323,6 +460,35 @@ script: - contextPath: SilentPush.DomainASNs.asns description: Dictionary of Autonomous System Numbers (ASNs) associated with the domain. type: Unknown + - deprecated: false + description: 'This command runs the threat check on the specified ' + name: silentpush-get-data-exports + arguments: + - name: feed_url + isArray: false + description: The URL from which to export the feed data. + required: true + secret: false + default: false + outputs: + - contextPath: SilentPush.GetDataExports.EntryID + description: The EntryID of the report file. + type: Unknown + - contextPath: SilentPush.GetDataExports.Extension + description: The extension of the report file. + type: String + - contextPath: SilentPush.GetDataExports.Name + description: The name of the report file. + type: String + - contextPath: SilentPush.GetDataExports.Info + description: The info of the report file. + type: String + - contextPath: SilentPush.GetDataExports.Size + description: The size of the report file. + type: Number + - contextPath: SilentPush.GetDataExports.Type + description: The type of the report file. + type: String - deprecated: false description: This command get certificate data collected from domain scanning. name: silentpush-get-domain-certificates @@ -475,7 +641,7 @@ script: default: false - name: value isArray: false - description: Value corresponding to the selected "resource" for which information needs to be retrieved{e.g. silentpush.com}. + description: Value corresponding to the selected "resource" for which information needs to be retrieved {e.g. silentpush.com}. required: true secret: false default: false @@ -517,7 +683,7 @@ script: description: Indicates if the domain is in the Alexa top 10k. type: Boolean - contextPath: SilentPush.Enrichment.domain_urls.results_summary.alexa_top10k_score - description: Score indicating domain’s Alexa top 10k ranking. + description: Score indicating domain's Alexa top 10k ranking. type: Number - contextPath: SilentPush.Enrichment.domain_urls.results_summary.dynamic_domain_score description: Score indicating likelihood of domain being dynamically generated. @@ -586,19 +752,19 @@ script: description: The number of distinct IP groups (e.g., IPs belonging to different ranges or providers). type: String - contextPath: SilentPush.Enrichment.ns_reputation.is_expired - description: Indicates if the domain’s nameserver is expired. + description: Indicates if the domain`s nameserver is expired. type: Boolean - contextPath: SilentPush.Enrichment.ns_reputation.is_parked description: ' The domain is not parked (a parked domain is one without active content).' type: Boolean - contextPath: SilentPush.Enrichment.ns_reputation.is_sinkholed - description: The domain is not sinkholed (not forcibly redirected to a security researcher’s trap). + description: The domain is not sinkholed (not forcibly redirected to a security researcher`s trap). type: Boolean - contextPath: SilentPush.Enrichment.ns_reputation.ns_reputation_max description: Maximum reputation score for nameservers. type: Number - contextPath: SilentPush.Enrichment.ns_reputation.ns_reputation_score - description: Reputation score of the domain’s nameservers. + description: Reputation score of the domain`s nameservers. type: Number - contextPath: SilentPush.Enrichment.ns_reputation.ns_srv_reputation.domain description: The nameservers of domain. @@ -1251,7 +1417,7 @@ script: description: Explanation of the geographic spread of the indicator as provided by the source. type: Unknown - deprecated: false - description: This command retrieve the reputation information for an IPv4. + description: This command retrieves the reputation information for an IPv4. name: silentpush-get-ipv4-reputation arguments: - name: ipv4 @@ -1333,7 +1499,7 @@ script: description: Current status of the job. type: String - deprecated: false - description: This command retrieve historical reputation data for a specified nameserver, including reputation scores and optional detailed calculation information. + description: This command retrieves historical reputation data for a specified nameserver,including reputation scores and optional detailed calculation information. name: silentpush-get-nameserver-reputation arguments: - name: nameserver @@ -1733,7 +1899,7 @@ script: description: List of VPN-related tags or null if not a VPN. type: Unknown - deprecated: false - description: This command scan a URL to retrieve hosting metadata.. + description: This command scan a URL to retrieve hosting metadata. name: silentpush-live-url-scan arguments: - name: url @@ -2183,6 +2349,44 @@ script: - contextPath: SilentPush.ReversePADNSLookup.records.type description: The type of DNS record (e.g., NS). type: String + - deprecated: false + description: 'This command runs the threat check on the specified ' + name: silentpush-run-threat-check + arguments: + - name: data + isArray: false + description: The name of the data source to query. + required: true + secret: false + default: false + - name: query + isArray: false + description: The value to check for threats (e.g., IP or domain). + required: true + secret: false + default: false + - name: type + isArray: false + description: The type of the value being queried (e.g., ip, domain). + required: true + secret: false + default: false + - name: user_identifier + isArray: false + description: A unique identifier for the user making the request. + required: true + secret: false + default: false + outputs: + - contextPath: SilentPush.RunThreatCheck.is_listed + description: Indicates whether the queried value is listed as a threat. + type: Boolean + - contextPath: SilentPush.RunThreatCheck.listed_txt + description: Textual description of the listing status. + type: String + - contextPath: SilentPush.RunThreatCheck.query + description: The original value that was checked. + type: String - deprecated: false description: This commandGenerate screenshot of a URL. name: silentpush-screenshot-url @@ -2537,12 +2741,16 @@ script: script: '-' type: python subtype: python3 - dockerimage: demisto/python3:3.12.8.3296088 + dockerimage: demisto/python3:3.12.11.3982393 feed: false isfetch: false runonce: false longRunning: false longRunningPort: false fromversion: 6.10.0 +sectionorder: + - Connect + - Collect + - Optimize tests: - No tests diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush_test.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush_test.py index e90a677d83cf..eac4350d5047 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush_test.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush_test.py @@ -6,7 +6,9 @@ you are implementing with your integration """ +import uuid import json +from requests import Response import pytest from SilentPush import ( Client, @@ -31,8 +33,21 @@ forward_padns_lookup_command, search_domains_command, density_lookup_command, + add_feed_command, + add_indicators_command, + add_indicators_tags_command, + get_data_exports_command, + run_threat_check_command, + add_feed_tags_command, + parse_arguments, + format_domain_information, + format_certificate_info, + validate_ip, + extract_and_sort_asn_reputation, + generate_no_reputation_response, + prepare_asn_reputation_table ) -from CommonServerPython import DemistoException +from CommonServerPython import DemistoException, EntryType def util_load_json(path): @@ -1348,3 +1363,297 @@ def test_density_lookup_command_api_error(mock_client): # Call the function and expect DemistoException with pytest.raises(DemistoException, match="API Error: Invalid query"): density_lookup_command(mock_client, args) + + +def test_add_feed_command_success(mock_client): + # Generate a unique name to avoid duplicates + unique_name = f"TestFeed_{uuid.uuid4().hex[:6]}" + + # Mock arguments passed to the command + args = {"name": unique_name, "type": "domain", "tags": "Test,Demo"} + expected_output = f"SilentPush feed: {unique_name} of type: domain was added successfully." + + # Simulated API response + mock_response = { + "name": unique_name, + "type": "domain", + "vendor": "SilentPush", + "feed_description": "Test feed for unit testing", + "category": "default", + "tags": "Test,Demo", + } + + # Patch the client's method to return the mocked response + mock_client.add_feed.return_value = mock_response + + # Call your command + result = add_feed_command(mock_client, args) + + # Assertions + assert isinstance(result, CommandResults) + assert result.outputs_prefix == "SilentPush.Feed" + assert result.outputs_key_field == "name" + assert result.outputs["name"] == unique_name + assert result.outputs["vendor"] == "SilentPush" + assert expected_output in result.readable_output + + +def test_add_indicators_command_success(mock_client): + # Generate unique UUID for test feed + test_uuid = str(uuid.uuid4()) + expected_output = f"Indicators: '['example.com', 'malicious.net']' were added successfully to SilentPush feed: '{test_uuid}'." + + # Mock arguments + args = {"feed_uuid": test_uuid, "indicators": ["example.com", "malicious.net"]} + + # Mock response returned by the API client + mock_response = {"feed_uuid": test_uuid, "added": 2, "indicators": ["example.com", "malicious.net"]} + + mock_client.add_indicators.return_value = {"created_or_updated": mock_response} + + # Execute command + result = add_indicators_command(mock_client, args) + + # Assertions + assert isinstance(result, CommandResults) + assert result.outputs_prefix == "SilentPush.AddIndicators" + assert result.outputs_key_field == "feed_uuid" + assert result.outputs["feed_uuid"] == test_uuid + assert result.outputs["added"] == 2 + assert expected_output in result.readable_output + + +def test_add_indicators_tags_command_success(mock_client): + test_uuid = str(uuid.uuid4()) + test_indicator = "example.com" + test_tags = ["test", "phishing"] + expected_output = f"Indicator Tags: '['test', 'phishing']' added to indicator 'example.com" + + args = {"feed_uuid": test_uuid, "indicator_name": test_indicator, "tags": test_tags} + + mock_response = {"feed_uuid": test_uuid, "indicator_name": test_indicator, "tags_added": test_tags} + + mock_client.add_indicators_tags.return_value = mock_response + + result = add_indicators_tags_command(mock_client, args) + + assert isinstance(result, CommandResults) + assert result.outputs_prefix == "SilentPush.AddIndicatorTags" + assert result.outputs_key_field == "feed_uuid" + assert result.outputs["feed_uuid"] == test_uuid + assert result.outputs["indicator_name"] == test_indicator + assert result.outputs["tags_added"] == test_tags + assert expected_output in result.readable_output + + +def test_run_threat_check_command_success(mock_client): + args = {"type": "domain", "data": ["example.com"], "user_identifier": "test_user", "query": "test_query"} + expected_output = "Threat check for query 'test_query' completed successfully" + + mock_response = { + "type": "domain", + "data": ["example.com"], + "user_identifier": "test_user", + "query": "test_query", + "result": "benign", + } + + mock_client.run_threat_check.return_value = mock_response + + result = run_threat_check_command(mock_client, args) + + assert isinstance(result, CommandResults) + assert result.outputs_prefix == "SilentPush.RunThreatCheck" + assert result.outputs_key_field == "query" + assert result.outputs["query"] == "test_query" + assert result.outputs["result"] == "benign" + assert expected_output in result.readable_output + + +def mock_file_response(content: bytes, status_code=200, headers=None) -> Response: + response = Response() + response.status_code = status_code + response._content = content + response.headers = headers or { + "Content-Disposition": 'attachment; filename="export.csv"', + "Content-Type": "application/octet-stream", + } + return response + + +def test_get_data_exports_command_success(mock_client): + feed_url = "https://api.silentpush.com/feeds/export.csv" + args = {"feed_url": feed_url} + content = b"test,data\n1,2" + + # Mock the response from the client (returns a real Response object) + mock_response = mock_file_response(content) + mock_client.get_data_exports.return_value = mock_response + + # Run the actual command function + result = get_data_exports_command(mock_client, args) + + # Assertions + assert isinstance(result, dict) + assert result["File"] == "export.csv" + assert result["Type"] == EntryType.ENTRY_INFO_FILE + + +def test_add_feed_tags_command_success(mocker): + # Mock args + args = {"feed_uuid": "abc123", "tags": "malware"} + expected_output = f"feed with uuid: abc123 was updated with tags: malware" + + # Mocked result returned by client.add_feed_tags + mock_response = {"created_or_updated": [{"uuid": "8eb9c1b8-edbb-4081-9978-590f5c5a0319", "tag": "phishing"}]} + expected_res = mock_response.get("created_or_updated") + + # Mock the Client and its method + mock_client = mocker.Mock(spec=Client) + mock_client.add_feed_tags.return_value = mock_response + + # Run the command + result = add_feed_tags_command(mock_client, args) + + # Assertions + assert isinstance(result, CommandResults) + assert result.outputs_prefix == "SilentPush.AddFeedTags" + assert result.outputs_key_field == "feed_uuid" + assert result.outputs == expected_res + assert result.raw_response == expected_res + assert expected_output in result.readable_output + +def test_parse_arguments_valid(): + """Test parsing with all valid arguments""" + args = { + "domains": "example.com, test.com", + "fetch_risk_score": "true", + "fetch_whois_info": "false" + } + + # Run the function + domains, fetch_risk_score, fetch_whois_info = parse_arguments(args) + + assert domains == ["example.com", "test.com"] + assert fetch_risk_score is True + assert fetch_whois_info is False + +def test_parse_arguments_no_domains(): + """Test when no domains are provided""" + args = {"fetch_risk_score": "true"} + with pytest.raises(DemistoException, match="No domains provided"): + parse_arguments(args) + +def test_format_domain_with_risk_and_whois(): + """Test formatting with risk score and WHOIS info""" + response = { + "domains": [ + { + "domain": "example.com", + "risk_score": 85, + "risk_score_explanation": "High reputation risk", + "whois_info": { + "registrant": "John Doe", + "country": "US" + }, + } + ] + } + + result = format_domain_information(response, fetch_risk_score=True, fetch_whois_info=True) + + # Assertions: check that key info is present in the markdown output + assert "# Domain Information Results" in result + assert "## Domain: example.com" in result + assert "Risk Assessment" in result + assert "John Doe" in result + assert "High reputation risk" in result + +def test_format_certificate_info_success(): + cert = { + "issuer": "Let's Encrypt", + "not_before": "2024-01-01", + "not_after": "2025-01-01", + "subject": str({"CN": "example.com"}), + "domains": ["example.com", "www.example.com"], + "serial_number": "1234567890", + "fingerprint_sha256": "abcdef1234567890" + } + + result = format_certificate_info(cert) + + assert result["Issuer"] == "Let's Encrypt" + assert result["Issued On"] == "2024-01-01" + assert result["Expires On"] == "2025-01-01" + assert result["Common Name"] == "example.com" + assert result["Subject Alternative Names"] == "example.com, www.example.com" + assert result["Serial Number"] == "1234567890" + assert result["Fingerprint SHA256"] == "abcdef1234567890" + +def test_validate_ip_invalid_ipv4(mock_client): + """Test validate_ip raises DemistoException for an invalid IPv4 address""" + resource = "ipv4" + value = "999.999.999.999" # invalid IPv4 + mock_client.validate_ip_address.return_value = False + + with pytest.raises(DemistoException, match=f"Invalid {resource.upper()} address: {value}"): + validate_ip(mock_client, resource, value) + +def test_extract_and_sort_asn_reputation(): + """Test ASN reputation extraction and sorting by date""" + raw_response = { + "response": { + "asn_reputation": [ + {"asn": "12345", "date": "2023-01-01", "score": 50}, + {"asn": "54321", "date": "2023-02-01", "score": 70}, + {"asn": "67890", "date": "2022-12-15", "score": 40}, + ] + } + } + + result = extract_and_sort_asn_reputation(raw_response) + + expected = [ + {"asn": "54321", "date": "2023-02-01", "score": 70}, # newest first + {"asn": "12345", "date": "2023-01-01", "score": 50}, + {"asn": "67890", "date": "2022-12-15", "score": 40}, # oldest last + ] + + assert result == expected + +def test_generate_no_reputation_response(): + asn = "AS12345" + raw_response = {"status": "success", "message": "No data available"} + + result = generate_no_reputation_response(asn, raw_response) + + # Validate type + assert isinstance(result, CommandResults) + + # Validate readable_output + assert result.readable_output == f"No reputation data found for ASN {asn}." + +def test_prepare_asn_reputation_table_with_explain(): + asn_reputation = [ + { + "asn": "AS12345", + "asn_reputation": "Good", + "asname": "Test ASN", + "date": "2025-09-01", + "asn_reputation_explain": "This ASN has a good reputation.", + } + ] + + result = prepare_asn_reputation_table(asn_reputation, explain=True) + + expected = [ + { + "ASN": "AS12345", + "Reputation": "Good", + "ASName": "Test ASN", + "Date": "2025-09-01", + "Explanation": "This ASN has a good reputation.", + } + ] + + assert result == expected \ No newline at end of file diff --git a/Packs/SilentPush/ReleaseNotes/1_2_0.md b/Packs/SilentPush/ReleaseNotes/1_2_0.md new file mode 100644 index 000000000000..9fac08614b1f --- /dev/null +++ b/Packs/SilentPush/ReleaseNotes/1_2_0.md @@ -0,0 +1,13 @@ + +#### Integrations + +##### SilentPush + +- Added support for **silentpush-run-threat-check** command that this command runs the threat check on the specified . +- Added support for **silentpush-add-feed-tags** command that this command add indicators to the feed. +- Added support for **silentpush-add-indicators** command that this command add indicators to the feed. +- Added support for **silentpush-add-indicator-tags** command that this command updates tags to the indicators. +- Added support for **silentpush-get-data-exports** command that this command runs the threat check on the specified . +- Added support for **silentpush-add-feed** command that this command add the new feed. +- Updated the Docker image to: *demisto/python3:3.12.11.3982393*. + diff --git a/Packs/SilentPush/pack_metadata.json b/Packs/SilentPush/pack_metadata.json index 0ce4021aa668..286b9562403c 100644 --- a/Packs/SilentPush/pack_metadata.json +++ b/Packs/SilentPush/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Silent Push", "description": "The Silent Push platform focuses on proactive threat intelligence and threat hunting.", "support": "partner", - "currentVersion": "1.1.0", + "currentVersion": "1.2.0", "author": "Silent Push", "url": "https://www.silentpush.com/contact/", "email": "integrations@silentpush.com",