diff --git a/.script/tests/KqlvalidationsTests/CustomFunctions/CensysRiskEvents.json b/.script/tests/KqlvalidationsTests/CustomFunctions/CensysRiskEvents.json new file mode 100644 index 00000000000..e880f10bb97 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomFunctions/CensysRiskEvents.json @@ -0,0 +1,174 @@ +{ + "FunctionName": "CensysRiskEvents", + "FunctionParameters": [], + "FunctionResultColumns": [ + { + "Name": "TimeGenerated", + "Type": "datetime" + }, + { + "Name": "IngestedAt", + "Type": "datetime" + }, + { + "Name": "EventId", + "Type": "string" + }, + { + "Name": "InsertedAt", + "Type": "datetime" + }, + { + "Name": "OccurredAt", + "Type": "datetime" + }, + { + "Name": "ChangeType", + "Type": "string" + }, + { + "Name": "RiskInstanceId", + "Type": "string" + }, + { + "Name": "SupplyChainId", + "Type": "string" + }, + { + "Name": "RiskTypeId", + "Type": "string" + }, + { + "Name": "CveId", + "Type": "string" + }, + { + "Name": "RiskCategory", + "Type": "string" + }, + { + "Name": "RiskName", + "Type": "string" + }, + { + "Name": "Description", + "Type": "string" + }, + { + "Name": "Remediations", + "Type": "dynamic" + }, + { + "Name": "Severity", + "Type": "string" + }, + { + "Name": "SeverityRank", + "Type": "long" + }, + { + "Name": "Score", + "Type": "real" + }, + { + "Name": "Confidence", + "Type": "real" + }, + { + "Name": "Status", + "Type": "string" + }, + { + "Name": "IsOpen", + "Type": "bool" + }, + { + "Name": "ResolveTime", + "Type": "datetime" + }, + { + "Name": "ResolveReason", + "Type": "string" + }, + { + "Name": "SupplierId", + "Type": "string" + }, + { + "Name": "SupplierDisplayName", + "Type": "string" + }, + { + "Name": "Suppliers", + "Type": "dynamic" + }, + { + "Name": "AssetType", + "Type": "string" + }, + { + "Name": "AssetName", + "Type": "string" + }, + { + "Name": "AssetIp", + "Type": "string" + }, + { + "Name": "AssetHostname", + "Type": "string" + }, + { + "Name": "AssetPort", + "Type": "int" + }, + { + "Name": "ServiceTransportProtocol", + "Type": "string" + }, + { + "Name": "ServicePort", + "Type": "int" + }, + { + "Name": "ServiceProtocol", + "Type": "string" + }, + { + "Name": "Tags", + "Type": "dynamic" + }, + { + "Name": "RapidResponse", + "Type": "bool" + }, + { + "Name": "Evidence", + "Type": "string" + }, + { + "Name": "FirstDetectedAt", + "Type": "datetime" + }, + { + "Name": "LastDetectedAt", + "Type": "datetime" + }, + { + "Name": "CreatedAt", + "Type": "datetime" + }, + { + "Name": "PlatformUrl", + "Type": "string" + }, + { + "Name": "EventVendor", + "Type": "string" + }, + { + "Name": "EventProduct", + "Type": "string" + } + ] +} diff --git a/.script/tests/KqlvalidationsTests/CustomTables/CensysRiskEvents_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/CensysRiskEvents_CL.json new file mode 100644 index 00000000000..4bc5cab7340 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/CensysRiskEvents_CL.json @@ -0,0 +1,149 @@ +{ + "Name": "CensysRiskEvents_CL", + "Properties": [ + { + "Name": "TimeGenerated", + "Type": "datetime" + }, + { + "Name": "EventId", + "Type": "string" + }, + { + "Name": "InsertedAt", + "Type": "datetime" + }, + { + "Name": "OccurredAt", + "Type": "datetime" + }, + { + "Name": "ChangeType", + "Type": "string" + }, + { + "Name": "RiskInstanceId", + "Type": "string" + }, + { + "Name": "SupplyChainId", + "Type": "string" + }, + { + "Name": "AssetType", + "Type": "string" + }, + { + "Name": "RiskTypeId", + "Type": "string" + }, + { + "Name": "CveId", + "Type": "string" + }, + { + "Name": "RiskCategory", + "Type": "string" + }, + { + "Name": "RiskName", + "Type": "string" + }, + { + "Name": "Description", + "Type": "string" + }, + { + "Name": "Remediations", + "Type": "dynamic" + }, + { + "Name": "Severity", + "Type": "string" + }, + { + "Name": "Score", + "Type": "real" + }, + { + "Name": "Confidence", + "Type": "real" + }, + { + "Name": "Status", + "Type": "string" + }, + { + "Name": "ResolveTime", + "Type": "datetime" + }, + { + "Name": "ResolveReason", + "Type": "string" + }, + { + "Name": "SupplierId", + "Type": "string" + }, + { + "Name": "SupplierDisplayName", + "Type": "string" + }, + { + "Name": "Suppliers", + "Type": "dynamic" + }, + { + "Name": "AssetIp", + "Type": "string" + }, + { + "Name": "AssetHostname", + "Type": "string" + }, + { + "Name": "AssetPort", + "Type": "int" + }, + { + "Name": "ServiceTransportProtocol", + "Type": "string" + }, + { + "Name": "ServicePort", + "Type": "int" + }, + { + "Name": "ServiceProtocol", + "Type": "string" + }, + { + "Name": "Tags", + "Type": "dynamic" + }, + { + "Name": "RapidResponse", + "Type": "bool" + }, + { + "Name": "Evidence", + "Type": "string" + }, + { + "Name": "FirstDetectedAt", + "Type": "datetime" + }, + { + "Name": "LastDetectedAt", + "Type": "datetime" + }, + { + "Name": "CreatedAt", + "Type": "datetime" + }, + { + "Name": "PlatformUrl", + "Type": "string" + } + ] +} diff --git a/.script/tests/detectionTemplateSchemaValidation/ValidConnectorIds.json b/.script/tests/detectionTemplateSchemaValidation/ValidConnectorIds.json index 880727c1fef..eab8dc1dd31 100644 --- a/.script/tests/detectionTemplateSchemaValidation/ValidConnectorIds.json +++ b/.script/tests/detectionTemplateSchemaValidation/ValidConnectorIds.json @@ -370,5 +370,6 @@ "RedRaysSAPSecurityFindings", "NetcraftIntegrationGatewayDefinition", "HoneyForge", - "QCryptonPQCScanner" + "QCryptonPQCScanner", + "CensysSRRiskEventsFunction" ] diff --git a/Sample Data/Custom/CensysRiskEvents_CL.csv b/Sample Data/Custom/CensysRiskEvents_CL.csv new file mode 100644 index 00000000000..3a6ed27cd41 --- /dev/null +++ b/Sample Data/Custom/CensysRiskEvents_CL.csv @@ -0,0 +1,19 @@ +TenantId,TimeGenerated [UTC],EventId,InsertedAt,OccurredAt,ChangeType,RiskInstanceId,SupplyChainId,AssetType,RiskTypeId,CveId,RiskCategory,RiskName,Description,Remediations,Severity,Score,Confidence,Status,ResolveTime,ResolveReason,SupplierId,SupplierDisplayName,Suppliers,AssetIp,AssetHostname,AssetPort,ServiceTransportProtocol,ServicePort,ServiceProtocol,Tags,RapidResponse,Evidence,FirstDetectedAt,LastDetectedAt,CreatedAt,PlatformUrl,Type +00000000-0000-0000-0000-000000000000,2026-10-04T14:43:07.075Z,70ee4ebe-701c-45dd-a06a-669454202dbb,2026-10-04T14:43:09.224Z,2026-10-04T14:43:07.075Z,resolved,7799ba98-6c16-49c9-a262-820cd60dbc4a,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,web,CENSYS-2026-1057,,EXPOSURE,Exposed Azure Client ID or Tenant ID,"An HTTP service is exposing an Azure Client ID or a Tenant ID, which may allow for unauthorized access to an Azure instance or resource if a secret is compromised.","[""No remediation is required solely because an Azure application client ID or tenant ID is publicly visible; these values are identifiers, not credentials."",""If a client secret, certificate private key, or access token is also exposed, remove it and revoke or rotate it immediately.""]",MEDIUM,0,0.5,closed,2026-10-04T14:35:54.599Z,asset_removed,e9dce30e-6c71-4188-a5b2-16c842ff9186,Fabrikam Cloud,"[{""id"":""e9dce30e-6c71-4188-a5b2-16c842ff9186"",""display_name"":""Fabrikam Cloud""}]",,host-3.example.com,443,,,,,false,,2026-09-10T18:10:32.966Z,2026-09-29T15:35:45.885Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/e9dce30e-6c71-4188-a5b2-16c842ff9186/web/host-3.example.com:443/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-07T22:03:49.140Z,818789a8-85ed-404c-b2e2-e1f533b9eaa8,2026-10-07T22:03:54.017Z,2026-10-07T22:03:49.140Z,resolved,656e4a22-7091-4707-b204-533c47b129fe,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2026-1196,,MISCONFIGURATION,Self-Signed Certificate,The SSL/TLS certificate was identified as signed by the owner of the application and not a trusted Certificate Authority.,"[""Request a new certificate from a public, trusted Certificate Authority and install it on your server. If possible, enable automatic renewal with your certificate provider.""]",LOW,0,1,closed,2026-10-07T21:46:52.653Z,service_removed,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""}]",192.0.2.3,,,TCP,2087,UNKNOWN,,false,,2026-10-06T18:52:22.993Z,2026-10-06T18:52:22.993Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/hosts/192.0.2.3/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-09T21:26:06.151Z,755f459a-af5f-4e1c-b890-f5862e5ccbf0,2026-10-09T21:26:14.086Z,2026-10-09T21:26:06.151Z,opened,48932552-75ce-4e06-9957-c4fff8720a9a,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2022-1034,,EXPOSURE,Exposed FTP Service,"This service transmits data in plaintext, which allows an attacker to intercept or modify the data if on the same network. FTP servers can also be targets of password spraying and credential stuffing attacks.","[""Cease using this service type. If that is not possible, restrict access to this service using firewall rules, VPN segmentation, or VPC segmentation to make it inaccessible from the Internet."",""If this service is needed externally, consider moving to sFTP and engaging an MFA provider to protect this service.""]",HIGH,0,1,open,,,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""}]",192.0.2.1,,,TCP,21,FTP,,false,,2026-10-09T19:30:52.880Z,2026-10-09T19:30:52.880Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/hosts/192.0.2.1/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-09T21:26:21.186Z,71dde199-f4ac-4e81-b3e2-8427a3908f12,2026-10-09T21:26:25.517Z,2026-10-09T21:26:21.186Z,opened,43edeca9-6f52-401b-9601-f38dc6dc354e,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CVE-2026-35385,CVE-2026-35385,VULNERABILITY,CVE-2026-35385,"In OpenSSH before 10.3, a file downloaded by scp may be installed setuid or setgid, an outcome contrary to some users' expectations, if the download is performed as root with -O (legacy scp protocol) and without -p (preserve mode).",,HIGH,7.4,0.85,open,,,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""},{""id"":""a7c93d1e-52b8-4f06-b1d4-0e6f2a8c9b35"",""display_name"":""Fabrikam Cloud""}]",192.0.2.7,,,TCP,2222,SSH,"[{""id"":""3b1f6c52-8a47-4d0e-9c61-5e2d7a9b4f13"",""display_name"":""Production""}]",true,OpenSSH_9.6p1 banner observed on 192.0.2.7:2222,2026-10-09T07:35:16.752Z,2026-10-09T07:35:16.752Z,2026-10-09T07:35:16.752Z,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/hosts/192.0.2.7/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-10T01:15:19.478Z,b23c9349-4574-4a97-8833-73da1646372f,2026-10-10T01:15:21.216Z,2026-10-10T01:15:19.478Z,resolved,e27f3dac-6c91-4d31-b168-f2c5faee9c16,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,web,CVE-2021-39275,CVE-2021-39275,VULNERABILITY,CVE-2021-39275,"ap_escape_quotes() may write beyond the end of a buffer when given malicious input. No included modules pass untrusted data to these functions, but third-party / external modules may. This issue affects Apache HTTP Server 2.4.48 and earlier.",,CRITICAL,0,0.25,closed,2026-10-10T01:01:58.191Z,asset_removed,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""}]",,host-1.example.com,443,,,,,false,,2026-10-06T14:12:09.509Z,2026-10-06T14:12:09.509Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/web/host-1.example.com:443/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-10T01:15:19.478Z,b23c9349-4574-4a97-8833-73da1646372f,2026-10-10T01:15:21.216Z,2026-10-10T01:15:19.478Z,resolved,e27f3dac-6c91-4d31-b168-f2c5faee9c16,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,web,CVE-2021-39275,CVE-2021-39275,VULNERABILITY,CVE-2021-39275,"ap_escape_quotes() may write beyond the end of a buffer when given malicious input. No included modules pass untrusted data to these functions, but third-party / external modules may. This issue affects Apache HTTP Server 2.4.48 and earlier.",,CRITICAL,0,0.25,closed,2026-10-10T01:01:58.191Z,web_property_removed,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""}]",,host-1.example.com,443,,,,,false,,2026-10-06T14:12:09.509Z,2026-10-06T14:12:09.509Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/web/host-1.example.com:443/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-15T18:51:56.421Z,5cd409a2-4154-497f-a252-c0de80328ec1,2026-10-15T18:55:14.491Z,2026-10-15T18:51:56.421Z,reopened,e09f3cb4-3b2c-4dfc-a8b6-f2abdfb35f1f,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,web,THREAT-0003,,THREAT,Gophish,"GoPhish is an open-source phishing framework designed for red teams and security professionals to simulate real-world phishing attacks. Built in Go, it provides an easy-to-use interface for creating, launching, and managing phishing campaigns to assess and improve organizational security awareness.",,UNSPECIFIED,0,0,open,,,e9dce30e-6c71-4188-a5b2-16c842ff9186,Fabrikam Cloud,"[{""id"":""e9dce30e-6c71-4188-a5b2-16c842ff9186"",""display_name"":""Fabrikam Cloud""}]",,host-2.example.com,443,,,,,false,,2026-10-07T20:00:23.960Z,2026-10-10T00:11:54.757Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/e9dce30e-6c71-4188-a5b2-16c842ff9186/web/host-2.example.com:443/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-15T21:27:29.306Z,c104cfb4-6c0e-4e86-a70d-81b97962faa3,2026-10-15T21:27:35.349Z,2026-10-15T21:27:29.306Z,reopened,9f044813-e1d1-4934-b2df-20699a2b0b91,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CVE-2026-44171,CVE-2026-44171,VULNERABILITY,CVE-2026-44171,"MariaDB server is a community developed fork of MySQL server. From versions 10.6.1 to before 10.6.26, 10.11.1 to before 10.11.17, 11.4.1 to before 11.4.11, 11.8.1 to before 11.8.7, and 12.3.1, mbstream did not check for /../ in the path when unpacking the archive. A proper backup can never contain such paths, but a specially crafted archive could have caused mbstream to create files outside of the target-dir path. This issue has been patched in versions 10.6.26, 10.11.17, 11.4.11, 11.8.7, and 12.3.2.",,MEDIUM,0,0.25,open,,,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""}]",192.0.2.1,,,TCP,3306,MYSQL,,false,,2026-10-09T09:35:28.213Z,2026-10-15T16:59:24.185Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/hosts/192.0.2.1/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-15T21:27:29.306Z,c1266028-f145-4155-98fb-4243e48dfebb,2026-10-15T21:27:35.364Z,2026-10-15T21:27:29.306Z,reopened,d3d60172-7d02-447a-83c4-a672c6e7244a,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2022-1192,,MISCONFIGURATION,Unencrypted IMAP Service,"This service is transmitting data in plaintext, which allows an attacker to intercept or modify transmitted data.","[""Secure this service with the latest TLS by using IMAPS."",""Disable the server if it isn't necessary.""]",MEDIUM,0,1,open,,,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""}]",192.0.2.1,,,TCP,143,IMAP,,false,,2026-10-09T18:44:05.705Z,2026-10-14T23:30:28.643Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/hosts/192.0.2.1/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-15T21:27:29.306Z,4960f709-a16f-49cd-a3ad-594fa2b1bb12,2026-10-15T21:27:35.381Z,2026-10-15T21:27:29.306Z,reopened,9848f7bc-83ba-4cd8-aee2-be030a572665,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2022-1193,,MISCONFIGURATION,Unencrypted POP3 Service,"This service is transmitting data in plaintext, which allows an attacker to intercept or modify transmitted data.","[""Secure this service with the latest TLS by using POP3S."",""Disable the server if it isn't necessary.""]",MEDIUM,0,1,open,,,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""}]",192.0.2.1,,,TCP,110,POP3,,false,,2026-10-08T21:01:57.366Z,2026-10-15T16:34:20.273Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/hosts/192.0.2.1/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-15T21:27:29.753Z,38edf9a3-6e65-4f18-b971-0504ae75030c,2026-10-15T21:27:35.531Z,2026-10-15T21:27:29.753Z,opened,02a4f9a0-2b6b-4121-8b9d-7d8b8203784e,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2024-1017,,MISCONFIGURATION,Strict-Transport-Security - includeSubDomains,This web service had an HSTS header that did not specify includeSubDomains.,"[""Add the includeSubDomains directive while following [a best practices guide](https://www.upguard.com/blog/hsts),\nalso slowly incrementing max-age while ensuring the web service and subdomains do not break due to increased\nrestrictions being imposed on clients' browsers.\n\nThe goal should be to have a header value similar to this, with a year or longer max-age in seconds:\n Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n""]",MEDIUM,0,1,open,,,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""}]",192.0.2.8,,,UDP,53,DNS,,false,,2026-10-15T19:28:01.182Z,2026-10-15T19:28:01.182Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/hosts/192.0.2.8/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-15T21:27:29.753Z,38edf9a3-6e65-4f18-b971-0504ae75030c,2026-10-15T21:27:35.531Z,2026-10-15T21:27:29.753Z,opened,02a4f9a0-2b6b-4121-8b9d-7d8b8203784e,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2024-1017,,MISCONFIGURATION,Strict-Transport-Security - includeSubDomains,This web service had an HSTS header that did not specify includeSubDomains.,"[""Add the includeSubDomains directive while following [a best practices guide](https://www.upguard.com/blog/hsts),\nalso slowly incrementing max-age while ensuring the web service and subdomains do not break due to increased\nrestrictions being imposed on clients' browsers.\n\nThe goal should be to have a header value similar to this, with a year or longer max-age in seconds:\n Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n""]",MEDIUM,0,1,open,,,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""}]",192.0.2.8,,,ICMP,1,ICMP,,false,,2026-10-15T19:28:01.182Z,2026-10-15T19:28:01.182Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/hosts/192.0.2.8/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-15T21:27:29.753Z,38edf9a3-6e65-4f18-b971-0504ae75030c,2026-10-15T21:27:35.531Z,2026-10-15T21:27:29.753Z,opened,02a4f9a0-2b6b-4121-8b9d-7d8b8203784e,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2024-1017,,MISCONFIGURATION,Strict-Transport-Security - includeSubDomains,This web service had an HSTS header that did not specify includeSubDomains.,"[""Add the includeSubDomains directive while following [a best practices guide](https://www.upguard.com/blog/hsts),\nalso slowly incrementing max-age while ensuring the web service and subdomains do not break due to increased\nrestrictions being imposed on clients' browsers.\n\nThe goal should be to have a header value similar to this, with a year or longer max-age in seconds:\n Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n""]",MEDIUM,0,1,open,,,a3842312-39c0-4189-b15c-7236889da34b,Contoso Analytics,"[{""id"":""a3842312-39c0-4189-b15c-7236889da34b"",""display_name"":""Contoso Analytics""}]",192.0.2.8,,,QUIC,443,HTTP3,,false,,2026-10-15T19:28:01.182Z,2026-10-15T19:28:01.182Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/a3842312-39c0-4189-b15c-7236889da34b/hosts/192.0.2.8/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-18T11:20:21.272Z,edbbe059-a2b2-46b7-8d80-cab12b3a96d1,2026-10-18T11:20:23.470Z,2026-10-18T11:20:21.272Z,reopened,ee5dfb4a-bcb6-4be7-8eb4-c3dba428eeea,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2022-1052,,EXPOSURE,Exposed PostgreSQL Service,"This service is designed to store potentially sensitive information in a database, which makes it a target. Password spraying and credential stuffing attacks against the service are common.","[""Restrict access to this service using firewall rules, VPN segmentation, or VPC segmentation to make it inaccessible from the Internet."",""Use one-way hashing encryption methods for columns that don't need to be unencrypted, like passwords. Restrict all non-required management ports.""]",HIGH,0,1,open,,,2fba9708-19cc-4501-98eb-f5151f38c9ec,Adventure Works,"[{""id"":""2fba9708-19cc-4501-98eb-f5151f38c9ec"",""display_name"":""Adventure Works""}]",192.0.2.4,,,TCP,5432,POSTGRES,,false,,2026-09-08T11:38:38.246Z,2026-10-18T09:42:22.710Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/2fba9708-19cc-4501-98eb-f5151f38c9ec/hosts/192.0.2.4/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-19T09:07:50.329Z,6f521712-78d6-466e-b218-e5e4426a8a1d,2026-10-19T09:07:54.459Z,2026-10-19T09:07:50.329Z,resolved,51b72199-feb1-4361-9fe6-e93917b9e949,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2026-1054,,EXPOSURE,Exposed CloudFlare API Token,"An HTTP service is exposing a CloudFlare API token, which may provide access to a CloudFlare account.","[""Remove the CloudFlare API token from the HTTP response of the affected service.""]",HIGH,0,0.75,closed,2026-10-19T09:00:04.806Z,service_removed,c5b3d1b0-8989-43ed-b37a-9d9e5e9aa7c7,Tailspin Software,"[{""id"":""c5b3d1b0-8989-43ed-b37a-9d9e5e9aa7c7"",""display_name"":""Tailspin Software""}]",192.0.2.6,,,TCP,443,HTTP,,false,,2026-10-05T06:24:44.924Z,2026-10-05T06:24:44.924Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/c5b3d1b0-8989-43ed-b37a-9d9e5e9aa7c7/hosts/192.0.2.6/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-22T03:10:13.952Z,a38a1253-5007-4e33-95aa-67323e90995e,2026-10-22T03:10:16.135Z,2026-10-22T03:10:13.952Z,resolved,c5fd1f93-1ac2-4de5-b423-980de69683b0,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2026-1169,,EXPOSURE,Exposed SonarQube Application,An HTTP service is exposing a SonarQube application. This web application is a static code analysis tool. The sensitive nature of the information contained in this application makes it a target.,"[""Restrict access to this application using firewall rules, VPN segmentation, or VPC segmentation to make it inaccessible from the Internet.""]",MEDIUM,0,0.75,closed,2026-10-22T03:00:33.074Z,finding_removed,ea4e3086-95cd-4365-b0a5-501aeb7f0575,Northwind Logistics,"[{""id"":""ea4e3086-95cd-4365-b0a5-501aeb7f0575"",""display_name"":""Northwind Logistics""}]",192.0.2.2,,,TCP,443,HTTP,,false,,2026-10-17T07:41:38.255Z,2026-10-21T17:00:26.701Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/ea4e3086-95cd-4365-b0a5-501aeb7f0575/hosts/192.0.2.2/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-22T06:36:13.849Z,6af978fd-4ab7-4759-bca5-6cebeca50cec,2026-10-22T06:36:20.257Z,2026-10-22T06:36:13.849Z,opened,a736fd70-36c2-4c7b-9a11-1b0e4ec3aeaf,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2022-1194,,MISCONFIGURATION,Unencrypted SMTP Service,"This service is transmitting data in plaintext, which allows an attacker to intercept or modify transmitted data.","[""Secure this service with the latest TLS by using SMTPS."",""Disable the server if it isn't necessary.""]",MEDIUM,0,1,open,,,2fba9708-19cc-4501-98eb-f5151f38c9ec,Adventure Works,"[{""id"":""2fba9708-19cc-4501-98eb-f5151f38c9ec"",""display_name"":""Adventure Works""}]",192.0.2.4,,,TCP,25,SMTP,,false,,2026-10-22T06:25:02.709Z,2026-10-22T06:25:02.709Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/2fba9708-19cc-4501-98eb-f5151f38c9ec/hosts/192.0.2.4/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL +00000000-0000-0000-0000-000000000000,2026-10-25T23:06:56.711Z,f35b7ae4-dbdb-44de-ac95-7cd7df6f316b,2026-10-25T23:06:58.986Z,2026-10-25T23:06:56.711Z,reopened,8a7f3482-22b2-4c70-a10c-4382f475734d,5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10,host,CENSYS-2022-1066,,EXPOSURE,Exposed SSH Service,"This service is designed to enable network communication between two devices, capability that makes it a target. Publicly available tools exist to target SSH services in password spraying and credential stuffing attacks.","[""Leverage a VPN or Zero Trust model to secure this SSH server."",""Follow traditional SSH hardening techniques, including Fail2Ban, MFA, user allow-lists, etc.""]",MEDIUM,0,1,open,,,ea4e3086-95cd-4365-b0a5-501aeb7f0575,Northwind Logistics,"[{""id"":""ea4e3086-95cd-4365-b0a5-501aeb7f0575"",""display_name"":""Northwind Logistics""}]",192.0.2.5,,,TCP,22,SSH,,false,,2026-10-17T02:15:23.529Z,2026-10-25T20:17:12.142Z,,https://platform.censys.io/sci/5f0b2d6e-3c1a-4b7e-9a2d-6e4f8c1b7a10/suppliers/ea4e3086-95cd-4365-b0a5-501aeb7f0575/hosts/192.0.2.5/risks?org=9c3e1f2a-7b4d-4e6a-8d5c-2a1b0f9e8d70,CensysRiskEvents_CL diff --git a/Solutions/Censys Supplier Risk/Analytic Rules/CensysRiskStateChange.yaml b/Solutions/Censys Supplier Risk/Analytic Rules/CensysRiskStateChange.yaml new file mode 100644 index 00000000000..3d73058e7bc --- /dev/null +++ b/Solutions/Censys Supplier Risk/Analytic Rules/CensysRiskStateChange.yaml @@ -0,0 +1,102 @@ +id: 8a208922-5ef7-4721-ad8b-7f69e2163d79 +name: Censys SR - Risk State Change +version: 1.0.0 +kind: Scheduled +status: Available +requiredDataConnectors: + - connectorId: CensysSRRiskEventsFunction + dataTypes: + - CensysRiskEvents_CL +queryFrequency: 1h +queryPeriod: 1d +suppressionDuration: PT1H +suppressionEnabled: false +triggerOperator: GreaterThan +triggerThreshold: 0 +tactics: + - InitialAccess +relevantTechniques: + - T1195 +severity: Medium +description: | + Creates one incident per supplier risk instance that is opened, or reopened, with CRITICAL or HIGH severity. Alerts are grouped by RiskInstanceId, so further events of the same risk instance within 24 hours are added to the same incident while it is open. + Alert severity follows the Censys severity: CRITICAL and HIGH map to High, MEDIUM to Medium and LOW to Low. Resolved events follow the same mapping, and any other severity is Informational. + By default the rule alerts only on new CRITICAL and HIGH risks. The query holds commented lines to follow the whole life of a risk instead (reopened events of any severity and resolved events), see the comments in the query. +query: | + // New events are selected by the time they reached Sentinel (IngestedAt), not by the Censys insert time, + // so a delay between Censys and the connector can never make an event fall between two runs. + // Each run covers 60 minutes that end 5 minutes ago, and consecutive runs are contiguous, so no event is alerted twice. + // + // OPTIONAL - follow the whole life of a risk instance (reopened events of any severity, and resolved events of risks that + // were alerted before). To use it, uncomment the two "let" lines below, comment out the line "Default" in the query, uncomment + // the line "Full lifecycle", and in the rule settings set the query period to 7 days and the incident grouping look-back to 7 days. + // let Alertable = materialize(CensysRiskEvents | where ChangeType == "reopened" or (ChangeType == "opened" and Severity in ("CRITICAL", "HIGH"))); + // let TrackedRisks = Alertable | distinct RiskInstanceId; + CensysRiskEvents + | where IngestedAt between (ago(65m) .. ago(5m)) + // Default: opened or reopened risks with CRITICAL or HIGH severity + | where ChangeType in ("opened", "reopened") and Severity in ("CRITICAL", "HIGH") + // Full lifecycle (needs the optional "let" lines above): + // | where (ChangeType == "reopened" or (ChangeType == "opened" and Severity in ("CRITICAL", "HIGH"))) or (ChangeType == "resolved" and RiskInstanceId in (TrackedRisks)) + // + // Optional filters: remove the leading // of a line to use it. Each line narrows the alerts further, and several lines can be combined. + // Do not remove or change the IngestedAt window above, it keeps every event alerted exactly once. + // | where Confidence >= 0.8 // only findings with a confidence of 80% or more + // | where Severity == "CRITICAL" // only CRITICAL risks (Censys severities: CRITICAL, HIGH, MEDIUM, LOW) + // | where ChangeType == "opened" // only newly opened risks, skip reopened risks + // | where RapidResponse == true // only actively exploited (rapid response) risks + // | where RiskCategory == "VULNERABILITY" // VULNERABILITY, MISCONFIGURATION, EXPOSURE or THREAT + // | where SupplierDisplayName in ("OpenAI", "Harvey") // only these suppliers + // | where CveId != "" // only risks that have a CVE id + // | where RiskName has "Log4Shell" // only risks whose name contains this text + // | where AssetType == "host" // asset type: host or web + | extend SentinelSeverity = case( + Severity in ("CRITICAL", "HIGH"), "High", + Severity == "MEDIUM", "Medium", + Severity == "LOW", "Low", + "Informational") + | project TimeGenerated, IngestedAt, EventId, InsertedAt, OccurredAt, ChangeType, RiskInstanceId, Severity, SentinelSeverity, + RiskCategory, RiskName, Description, CveId, RapidResponse, SupplierDisplayName, + AssetName, AssetHostname, AssetIp, ServicePort, ServiceProtocol, Evidence, ResolveTime, ResolveReason, PlatformUrl +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: AssetHostname + - entityType: IP + fieldMappings: + - identifier: Address + columnName: AssetIp + - entityType: URL + fieldMappings: + - identifier: Url + columnName: PlatformUrl +eventGroupingSettings: + aggregationKind: AlertPerResult +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: P1D + matchingMethod: Selected + groupByCustomDetails: + - RiskInstanceId +customDetails: + RiskInstanceId: RiskInstanceId + ChangeType: ChangeType + CensysSeverity: Severity + RiskCategory: RiskCategory + Supplier: SupplierDisplayName + CveId: CveId + RapidResponse: RapidResponse + Evidence: Evidence + ResolveReason: ResolveReason + ResolveTime: ResolveTime +alertDetailsOverride: + alertDisplayNameFormat: "Censys: {{ChangeType}} risk - {{RiskName}} ({{SupplierDisplayName}})" + alertDescriptionFormat: "{{Description}} Asset: {{AssetName}}. Open in Censys: {{PlatformUrl}}" + alertSeverityColumnName: SentinelSeverity + alertDynamicProperties: + - alertProperty: AlertLink + value: PlatformUrl diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSR100.zip b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSR100.zip new file mode 100644 index 00000000000..ac71d537ad4 Binary files /dev/null and b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSR100.zip differ diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRRiskEvents/__init__.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRRiskEvents/__init__.py new file mode 100644 index 00000000000..ccbc09b90df --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRRiskEvents/__init__.py @@ -0,0 +1,129 @@ +"""Timer triggered function: ingest Censys risk events into Microsoft Sentinel.""" +import datetime +import time +from typing import List, Optional + +import azure.functions as func + +from ..SharedCode import consts +from ..SharedCode.censys_client import CensysClient +from ..SharedCode.checkpoint import CheckpointManager, checkpoint_key +from ..SharedCode.exceptions import CensysCursorExpired, CensysException, CensysTimeBudgetReached +from ..SharedCode.logger import applogger +from ..SharedCode.sentinel import SentinelIngestor +from ..SharedCode.suppliers import RiskStore, SupplierStore, resolve +from ..SharedCode.validation import Settings, load_settings + +_RFC3339 = "%Y-%m-%dT%H:%M:%SZ" + + +def _supplier_ids(settings: Settings) -> Optional[List[str]]: + """Look up the ids of the supplier names, once per run. Returns None when no supplier names are set. + + A supplier added to Censys later is used from the next run on, without reading its earlier events. A name that + matches no supplier is a warning, not an error. An empty list means that no id was found at all. + """ + if not settings.supplier_names: + return None + store = SupplierStore(settings.storage_connection, consts.SUPPLIERS_TABLE_NAME, settings.organization_id) + ids, missing = resolve(settings.supplier_names, store.load()) + for name in missing: + applogger.warning(f"{consts.LOG_PREFIX} ingest: supplier name '{name}' was not found " + "in the suppliers table, it is skipped") + if len(ids) > consts.MAX_SUPPLIER_IDS: + raise CensysException(f"The supplier names in SupplierNames match {len(ids)} suppliers, at most " + f"{consts.MAX_SUPPLIER_IDS} are allowed in one request. Enter fewer names.") + return ids + + +def _risk_type_ids(settings: Settings) -> Optional[List[str]]: + """Look up the ids of the risk names, once per run. Returns None when no risk names are set. + + Works like _supplier_ids: an unknown name is a warning, and an empty list means that no id was found at all. + """ + if not settings.risk_names: + return None + store = RiskStore(settings.storage_connection, consts.RISKS_TABLE_NAME, settings.organization_id) + ids, missing = resolve(settings.risk_names, store.load()) + for name in missing: + applogger.warning(f"{consts.LOG_PREFIX} ingest: risk name '{name}' was not found " + "in the risks table, it is skipped") + if len(ids) > consts.MAX_RISK_TYPE_IDS: + raise CensysException(f"The risk names in RiskNames match {len(ids)} risks, at most " + f"{consts.MAX_RISK_TYPE_IDS} are allowed in one request. Enter fewer names.") + return ids + + +def ingest(start: float) -> int: + """Read every new event, upload it page by page and move the checkpoint after each page.""" + settings = load_settings() + supplier_ids = _supplier_ids(settings) + risk_type_ids = _risk_type_ids(settings) + for setting, ids in (("SupplierNames", supplier_ids), ("RiskNames", risk_type_ids)): + if ids is not None and not ids: + applogger.warning(f"{consts.LOG_PREFIX} ingest: no id found for the names set in {setting}, " + "nothing is collected in this run. The next run tries again.") + return 0 + filters = settings.filters(supplier_ids, risk_type_ids) + # The suppliers and risks are not part of the checkpoint key. When their names are changed, collection continues + # from the current checkpoint with the new ids, and the earlier events of a newly selected supplier or risk + # are not collected. + key_parts = [f"{k}={v}" for k, v in sorted(filters.items()) if k not in ("supplier_ids", "risk_type_ids")] + key = checkpoint_key(settings.organization_id, *key_parts) + manager = CheckpointManager(settings.storage_connection, consts.CHECKPOINT_TABLE_NAME) + client = CensysClient(settings.api_token, settings.organization_id, deadline=start + consts.MAX_RUN_SECONDS, + hard_deadline=start + consts.HARD_LIMIT_SECONDS) + ingestor = SentinelIngestor(settings.data_collection_endpoint, settings.data_collection_rule_id, + consts.STREAM_NAME, consts.SCOPE) + + checkpoint = manager.get(key) + if checkpoint and checkpoint[0]: + after_time, after_id = checkpoint + applogger.info(f"{consts.LOG_PREFIX} ingest: continuing from checkpoint " + f"inserted_at={after_time} id={after_id}") + else: + after_time, after_id = settings.start_time, None + applogger.info(f"{consts.LOG_PREFIX} ingest: no checkpoint, starting from {after_time}") + + total, expired = 0, False + while True: + try: + for events in client.iter_pages(after_time, after_id, filters): + ingestor.upload(events) + total += len(events) + last = events[-1] + if last.get("inserted_at") and last.get("id"): + manager.set(key, last["inserted_at"], last["id"]) + else: + applogger.warning(f"{consts.LOG_PREFIX} ingest: last event of the page has no inserted_at " + "or id, checkpoint not moved") + applogger.info(f"{consts.LOG_PREFIX} ingest: ingested {len(events)} events, " + f"checkpoint inserted_at={last.get('inserted_at')}") + return total + except CensysTimeBudgetReached: + applogger.info(f"{consts.LOG_PREFIX} ingest: time budget of {consts.MAX_RUN_SECONDS} seconds reached " + f"after {total} events. The checkpoint is saved and the next run continues from it.") + return total + except CensysCursorExpired: + if expired: + raise + expired = True + window = datetime.timedelta(days=consts.RETENTION_DAYS) + after_time = (datetime.datetime.now(datetime.timezone.utc) - window).strftime(_RFC3339) + after_id = None + applogger.warning(f"{consts.LOG_PREFIX} ingest: the checkpoint is older than the retention window, " + f"restarting from {after_time}") + + +def main(mytimer: func.TimerRequest) -> None: + """Entry point called by the timer trigger.""" + start = time.time() + applogger.info(f"{consts.LOG_PREFIX} main: start processing") + if mytimer.past_due: + applogger.info(f"{consts.LOG_PREFIX} main: the timer is past due") + try: + total = ingest(start) + except CensysException as err: + applogger.error(f"{consts.LOG_PREFIX} main: run failed: {err}") + raise + applogger.info(f"{consts.LOG_PREFIX} main: finished, {total} events ingested in {int(time.time() - start)} sec") diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRRiskEvents/function.json b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRRiskEvents/function.json new file mode 100644 index 00000000000..34c9c78d63b --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRRiskEvents/function.json @@ -0,0 +1,11 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "name": "mytimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "%Schedule%" + } + ] +} diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRSuppliersAndRisks/__init__.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRSuppliersAndRisks/__init__.py new file mode 100644 index 00000000000..d14b063eded --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRSuppliersAndRisks/__init__.py @@ -0,0 +1,67 @@ +"""Timer triggered function: keep the supplier names and risk names with their ids in storage tables. + +Nothing is sent to Microsoft Sentinel. +""" +import time +from typing import Tuple + +import azure.functions as func + +from ..SharedCode import consts +from ..SharedCode.censys_client import CensysClient +from ..SharedCode.exceptions import CensysException +from ..SharedCode.logger import applogger +from ..SharedCode.suppliers import RiskStore, SupplierStore +from ..SharedCode.validation import load_common + + +def _client(token: str, organization_id: str, start: float) -> CensysClient: + """Create a client that stays within the time budget of the run.""" + return CensysClient(token, organization_id, deadline=start + consts.MAX_RUN_SECONDS, + hard_deadline=start + consts.HARD_LIMIT_SECONDS) + + +def sync_suppliers(start: float) -> int: + """Read all suppliers from Censys and store their ids and display names. Returns the number of suppliers.""" + token, organization_id, storage = load_common() + store = SupplierStore(storage, consts.SUPPLIERS_TABLE_NAME, organization_id) + stored, removed = store.sync(_client(token, organization_id, start).iter_suppliers()) + applogger.info(f"{consts.LOG_PREFIX} sync_suppliers: suppliers table updated, " + f"{stored} suppliers stored, {removed} removed") + return stored + + +def sync_risks(start: float) -> int: + """Read all risks from Censys and store their ids and names. Returns the number of risks.""" + token, organization_id, storage = load_common() + store = RiskStore(storage, consts.RISKS_TABLE_NAME, organization_id) + stored, removed = store.sync(_client(token, organization_id, start).iter_risks()) + applogger.info(f"{consts.LOG_PREFIX} sync_risks: risks table updated, {stored} risks stored, {removed} removed") + return stored + + +def sync_all(start: float) -> Tuple[int, int]: + """Sync the suppliers and the risks. A failure of one does not stop the other, and any failure is raised.""" + counts, errors = [], [] + for name, sync in (("suppliers", sync_suppliers), ("risks", sync_risks)): + try: + counts.append(sync(start)) + except CensysException as err: + applogger.error(f"{consts.LOG_PREFIX} sync_all: {name} sync failed: {err}") + errors.append(f"{name}: {err}") + if errors: + raise CensysException("Sync failed for " + "; ".join(errors)) + return counts[0], counts[1] + + +def main(mytimer: func.TimerRequest) -> None: + """Entry point called by the timer trigger.""" + start = time.time() + applogger.info(f"{consts.LOG_PREFIX} main: suppliers and risks sync start processing") + try: + suppliers, risks = sync_all(start) + except CensysException as err: + applogger.error(f"{consts.LOG_PREFIX} main: suppliers and risks sync failed: {err}") + raise + applogger.info(f"{consts.LOG_PREFIX} main: suppliers and risks sync finished, {suppliers} suppliers and " + f"{risks} risks in {int(time.time() - start)} sec") diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRSuppliersAndRisks/function.json b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRSuppliersAndRisks/function.json new file mode 100644 index 00000000000..09a1e66a103 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSRSuppliersAndRisks/function.json @@ -0,0 +1,12 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "name": "mytimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "%SuppliersAndRisksSchedule%", + "runOnStartup": true + } + ] +} diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSR_RiskEvents_FunctionApp.json b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSR_RiskEvents_FunctionApp.json new file mode 100644 index 00000000000..841c16913f1 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/CensysSR_RiskEvents_FunctionApp.json @@ -0,0 +1,132 @@ +{ + "id": "CensysSRRiskEventsFunction", + "title": "Censys Supplier Risk (using Azure Functions)", + "publisher": "Censys", + "descriptionMarkdown": "The Censys Supplier Risk data connector brings the Censys risk-events change feed (`/v1/supplychains/default/risk-events`) into Microsoft Sentinel using Azure Functions and the [Logs Ingestion API](https://learn.microsoft.com/azure/azure-monitor/logs/logs-ingestion-api-overview). The function runs on a schedule and remembers the `inserted_at` and `id` of the last event it ingested, so each run continues where the previous one stopped. Events are written to the table `CensysRiskEvents_CL` through a data collection endpoint and a data collection rule that the deployment template creates. Suppliers and risks can be selected by name; the connector keeps the supplier names and risk names with their ids in storage tables.", + "graphQueries": [ + { + "metricName": "Total Censys risk events received", + "legend": "CensysRiskEvents_CL", + "baseQuery": "CensysRiskEvents_CL" + } + ], + "sampleQueries": [ + { + "description": "All Censys risk events received, newest first.", + "query": "CensysRiskEvents_CL\n| sort by TimeGenerated desc" + }, + { + "description": "Newly opened or reopened risks with critical or high severity.", + "query": "CensysRiskEvents_CL\n| where ChangeType in ('opened', 'reopened')\n| where Severity in ('CRITICAL', 'HIGH')\n| project TimeGenerated, Severity, RiskName, SupplierDisplayName, AssetIp, AssetHostname, PlatformUrl\n| order by TimeGenerated desc" + }, + { + "description": "Rapid response findings (actively exploited).", + "query": "CensysRiskEvents_CL\n| where RapidResponse == true and ChangeType in ('opened', 'reopened')\n| project TimeGenerated, Severity, CveId, RiskName, SupplierDisplayName, AssetIp, Evidence, PlatformUrl" + }, + { + "description": "Currently open risks per supplier (latest event per risk).", + "query": "CensysRiskEvents_CL\n| summarize arg_max(InsertedAt, *) by RiskInstanceId\n| where Status == 'open'\n| summarize OpenRisks = count() by SupplierDisplayName, Severity\n| order by OpenRisks desc" + }, + { + "description": "Risk events mapped to known CVEs.", + "query": "CensysRiskEvents_CL\n| where isnotempty(CveId)\n| summarize AffectedAssets = dcount(RiskInstanceId) by CveId, Severity\n| order by AffectedAssets desc" + } + ], + "dataTypes": [ + { + "name": "CensysRiskEvents_CL", + "lastDataReceivedQuery": "CensysRiskEvents_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + } + ], + "connectivityCriterias": [ + { + "type": "IsConnectedQuery", + "value": [ + "CensysRiskEvents_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + } + ], + "availability": { + "status": 1, + "isPreview": true + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "read and write permissions on the workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ], + "customs": [ + { + "name": "Microsoft.Web/sites permissions", + "description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)." + }, + { + "name": "REST API Credentials/permissions", + "description": "A Censys personal access token and the Censys Organization ID are required. Users on Starter, Search, and Core plans need the API Access role. [Create a token](https://accounts.censys.io/settings/personal-access-tokens)." + }, + { + "name": "Microsoft Entra application", + "description": "An app registration with a client secret is required. Permission to create role assignments on the data collection rule (Owner or User Access Administrator) is required to give the application the Monitoring Metrics Publisher role." + } + ] + }, + "instructionSteps": [ + { + "title": "", + "description": ">**NOTE:** This connector uses Azure Functions to connect to the Censys API to pull risk events into Microsoft Sentinel using the [Logs Ingestion API](https://learn.microsoft.com/azure/azure-monitor/logs/logs-ingestion-api-overview). The deployment template creates the table CensysRiskEvents_CL, a data collection endpoint and a data collection rule in the workspace. A second function, CensysSRSuppliersAndRisks, keeps the lists of supplier names and risk names, with their ids, in storage tables of the Function App (nothing is sent to Microsoft Sentinel), so supplier names and risk names can be used as an input. This might result in additional data ingestion costs. Check the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/) for details." + }, + { + "title": "", + "description": ">**(Optional Step)** Securely store the Censys API token and the client secret in Azure Key Vault. Azure Key Vault provides a secure mechanism to store and retrieve key values. [Follow these instructions](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) to use Azure Key Vault with an Azure Function App." + }, + { + "title": "", + "description": "**STEP 1 - Steps to Create/Get a Censys Personal Access Token and Organization ID**\n\n Follow these instructions to get the values needed by this connector:\n 1. Sign in to Censys and open [Personal access tokens](https://accounts.censys.io/settings/personal-access-tokens).\n 2. Create a personal access token. Users on Starter, Search, and Core plans need the **API Access** role.\n 3. Copy the token. It is shown only once.\n 4. Copy the **Organization ID** (UUID) of the Censys organization you want to collect risk events for." + }, + { + "title": "", + "description": "**STEP 2 - Get the name of the Log Analytics workspace**\n\n The deployment template creates the table, the data collection endpoint and the data collection rule in the workspace of Microsoft Sentinel, so deploy it in the resource group of that workspace. You need the **name** of the workspace (workspace Overview page)." + }, + { + "title": "", + "description": "**STEP 3 - App Registration steps for the Application in Microsoft Entra ID**\n\n This integration requires an App registration in the Azure portal. Follow the steps in this section to create a new application in Microsoft Entra ID:\n 1. Sign in to the [Azure portal](https://portal.azure.com/).\n 2. Search for and select **Microsoft Entra ID**.\n 3. Under **Manage**, select **App registrations > New registration**.\n 4. Enter a display **Name** for your application.\n 5. Select **Register** to complete the initial app registration.\n 6. When registration finishes, the Azure portal displays the app registration's Overview pane. You see the **Application (client) ID** and **Tenant ID**. The client ID and Tenant ID are required as configuration parameters for the execution of the Censys SR data connector. \n\n> **Reference link:** [https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app)" + }, + { + "title": "", + "description": "**STEP 4 - Add a client secret for application in Microsoft Entra ID**\n\n Sometimes called an application password, a client secret is a string value required for the execution of the Censys SR data connector. Follow the steps in this section to create a new Client Secret:\n 1. In the Azure portal, in **App registrations**, select your application.\n 2. Select **Certificates & secrets > Client secrets > New client secret**.\n 3. Add a description for your client secret.\n 4. Select an expiration for the secret or specify a custom lifetime. Limit is 24 months.\n 5. Select **Add**. \n 6. *Record the secret's value for use in your client application code. This secret value is never displayed again after you leave this page.* The secret value is required as configuration parameter for the execution of the Censys SR data connector. \n\n> **Reference link:** [https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app#add-a-client-secret](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app#add-a-client-secret)" + }, + { + "title": "", + "description": "**STEP 5 - Get Object ID of your application in Microsoft Entra ID**\n\n After creating your app registration, follow the steps in this section to get Object ID:\n 1. Go to **Microsoft Entra ID**.\n 2. Select **Enterprise applications** from the left menu.\n 3. Find your newly created application in the list (you can search by the name you provided).\n 4. Click on the application.\n 5. On the overview page, copy the **Object ID**. This is the **Azure_Entra_Object_Id** needed for your ARM template role assignment. The template gives this application the **Monitoring Metrics Publisher** role on the data collection rule, so no other role needs to be assigned." + }, + { + "title": "", + "description": "**STEP 6 - Choose ONE from the following two deployment options to deploy the connector and the associated Azure Function**\n\n>**IMPORTANT:** Before deploying the Censys SR data connector, have the Censys API token, the organization ID, the workspace name and the Azure credentials (client ID, client secret, tenant ID, object ID) ready." + }, + { + "title": "Option 1 - Azure Resource Manager (ARM) Template", + "description": "Use this method for automated deployment of the Censys SR connector.\n\n1. Click the **Deploy to Azure** button below. \n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-CensysSRRiskEventsFunction-azuredeploy)\n2. Select the preferred **Subscription** and **Resource Group**. Use the resource group of the Log Analytics workspace of Microsoft Sentinel. \n3. Enter the below information : \n\n\t a. **Censys SR Function Name** - Name prefix of the Azure Function App to be created, up to 10 characters. Default is CensysSR.\n\n\t b. **Censys Api Key** - Enter your Censys personal access token.\n\n\t c. **Censys Organization Id** - Enter the UUID of the Censys organization to collect risk events for, for example 00000000-0000-0000-0000-000000000000.\n\n\t d. **Censys Start Date Time** - Optional. UTC time to start collecting from, in the form 2026-10-01T00:00:00Z. It must not be in the future. Leave empty for full history (all events Censys still retains).\n\n\t e. **Censys Supplier Names** - Optional. Names of the suppliers to collect risk events for, separated by an asterisk (*). For example: OpenAI*Harvey*Acme, Inc. Names are not case-sensitive and can be picked from the Censys Supplier Risk UI. Names that are not found among your suppliers are skipped with a warning. Leave empty to collect all suppliers. A change applies from the next run and continues from the current position, so avoid changing it often. Maximum 500 characters.\n\n\t f. **Censys Risk Names** - Optional. Names of the risks to collect risk events for, separated by an asterisk (*). For example: Log4Shell*Exposed Redis. Names are not case-sensitive and must match the risk name shown in the Censys Supplier Risk UI. Names that are not found among the risks of your supply chain are skipped with a warning. Leave empty to collect all risks. A change applies from the next run and continues from the current position, so avoid changing it often. Maximum 500 characters.\n\n\t g. **Censys Change Types** - Optional. Comma-separated change types: opened, resolved, reopened. Other values stop the run with an error. Leave empty for all.\n\n\t h. **Censys Severity** - Optional. Comma-separated severities: CRITICAL, HIGH, MEDIUM, LOW. Other values stop the run with an error. Leave empty for all.\n\n\t i. **Censys Rapid Response** - Optional. Select true for rapid response findings only, false to exclude them. Leave empty for both.\n\n\t j. **Azure_Client_Id** - Enter the Azure client ID of the app registration you created.\n\n\t k. **Azure_Client_Secret** - Enter the client secret you created for the app registration.\n\n\t l. **Azure_Tenant_Id** - Enter the Azure tenant ID of your Microsoft Entra ID.\n\n\t m. **Azure_Entra_Object_Id** - Enter the object ID of your Microsoft Entra app (Enterprise application).\n\n\t n. **WorkspaceName** - Name of the Log Analytics workspace of Microsoft Sentinel. The deployment creates the table CensysRiskEvents_CL, a data collection endpoint and a data collection rule in it.\n\n\t o. **Location** - Region of the data collection endpoint and rule. It must be the region of the workspace. Default is the region of the resource group.\n\n\t p. **Censys Risk Events Table Name** - Name of the table in the workspace that the risk events are written to. Default is CensysRiskEvents_CL (the suffix _CL is added, or corrected, when it is missing or in another case). Maximum 52 characters. Warning: the parser, the workbook, the analytic rule and the connector status check of this solution read CensysRiskEvents_CL. If you use another name, update these resources to read the new table.\n\n\t q. **Censys Risk Events Collection Schedule** - Please enter a valid Quartz cron-expression, for example 0 */10 * * * *. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is 0 */10 * * * * (every 10 minutes).\n\n\t r. **Censys Supplier and Risk List Refresh Schedule** - Quartz cron-expression for how often the function that refreshes the lists of supplier names and risk names with their ids runs, for example 0 */30 * * * *. It also runs once when the app starts. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is every 30 minutes.\n\n\t s. **AppInsightsWorkspaceResourceID** - Use 'Log Analytic Workspace > Properties' blade having 'Resource ID' property value. This is a fully qualified resource ID which is in format '/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/Microsoft.OperationalInsights/workspaces/{workspace-name}'.\n\n\t t. **Censys SR Function Log Level** - Select log level or log severity value from DEBUG, INFO, ERROR. By default it is set to INFO.\n\n4. Mark the checkbox labeled **I agree to the terms and conditions stated above**. \n5. Click **Purchase** to deploy." + }, + { + "title": "Option 2 - Manual Deployment of Azure Functions", + "description": "Use the following step-by-step instructions to deploy the Censys SR data connector manually with Azure Functions (Deployment via Visual Studio Code)." + }, + { + "title": "", + "description": "**1. Deploy a Function App**\n\n> **NOTE:** You will need to [prepare VS code](https://docs.microsoft.com/azure/azure-functions/functions-create-first-function-python#prerequisites) for Azure function development.\n\n1. Download the [Azure Function App](https://aka.ms/sentinel-CensysSR100-functionapp) file. Extract archive to your local development computer.\n2. Start VS Code. Choose File in the main menu and select Open Folder.\n3. Select the top level folder from extracted files.\n4. Choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose the **Deploy to function app** button.\nIf you aren't already signed in, choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose **Sign in to Azure**\nIf you're already signed in, go to the next step.\n5. Provide the following information at the prompts:\n\n\ta. **Select folder:** Choose a folder from your workspace or browse to one that contains your function app.\n\n\tb. **Select Subscription:** Choose the subscription to use.\n\n\tc. Select **Create new Function App in Azure** (Don't choose the Advanced option)\n\n\td. **Enter a globally unique name for the function app:** Type a name that is valid in a URL path. The name you type is validated to make sure that it's unique in Azure Functions. (e.g. CensysSRXXXXX).\n\n\te. **Select a runtime:** Choose Python 3.14 (the packages in the zip are built for it).\n\n\tf. Select a location for new resources. For better performance and lower costs choose the same [region](https://azure.microsoft.com/regions/) where Microsoft Sentinel is located.\n\n6. Deployment will begin. A notification is displayed after your function app is created and the deployment package is applied.\n7. Go to Azure Portal for the Function App configuration.\n8. In the Function App, enable the system-assigned managed identity if you want to use it for the function app storage.\n9. The manual method needs the table CensysRiskEvents_CL, a data collection endpoint and a data collection rule. Deploy the template of Option 1 once to create them, or create them yourself.\n10. Give the Microsoft Entra application the **Monitoring Metrics Publisher** role on the data collection rule: open the rule, select **Access control (IAM) > Add role assignment**, choose the role and select your application." + }, + { + "title": "", + "description": "**2. Configure the Function App**\n\n1. In the Function App, select the Function App Name and select **Configuration**.\n2. In the **Application settings** tab, select **+ New application setting**.\n3. Add each of the following application settings individually, with their respective values (case-sensitive): \n\n\t a. **CensysApiToken** - Enter your Censys personal access token.\n\n\t b. **OrganizationId** - Enter the UUID of the Censys organization, for example 00000000-0000-0000-0000-000000000000.\n\n\t c. **InitialStartTime** - Optional. UTC start time in the form 2026-10-01T00:00:00Z. It must not be in the future. Leave empty for all events Censys still retains.\n\n\t d. **SupplierNames** - Optional. Supplier names separated by an asterisk (*), for example OpenAI*Harvey*Acme, Inc. Pick the names from the Censys Supplier Risk UI. Maximum 500 characters.\n\n\t e. **RiskNames** - Optional. Risk names separated by an asterisk (*), for example Log4Shell*Exposed Redis. Pick the names from the Censys Supplier Risk UI. Maximum 500 characters.\n\n\t f. **ChangeTypes** - Optional. Comma-separated: opened, resolved, reopened. Other values stop the run with an error.\n\n\t g. **Severity** - Optional. Comma-separated: CRITICAL, HIGH, MEDIUM, LOW. Other values stop the run with an error.\n\n\t h. **RapidResponse** - Optional. true or false.\n\n\t i. **AZURE_CLIENT_ID** - Azure Client Id of your Microsoft Entra app.\n\n\t j. **AZURE_CLIENT_SECRET** - Client secret of your Microsoft Entra app.\n\n\t k. **AZURE_TENANT_ID** - Azure Tenant Id of your Microsoft Entra ID.\n\n\t l. **AZURE_DATA_COLLECTION_ENDPOINT** - Logs ingestion URL of the data collection endpoint created for this connector (Monitor > Data Collection Endpoints > the endpoint > Logs Ingestion).\n\n\t m. **AZURE_DATA_COLLECTION_RULE_ID** - Immutable ID of the data collection rule created for this connector (Monitor > Data Collection Rules > the rule > JSON View > immutableId).\n\n\t n. **StreamName** - Input stream of that data collection rule, Custom-CensysRiskEvents_CL.\n\n\t o. **SCOPE** - https://monitor.azure.com/.default\n\n\t p. **Schedule** - Quartz cron-expression, for example 0 */10 * * * *. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is 0 */10 * * * *.\n\n\t q. **SuppliersAndRisksSchedule** - Quartz cron-expression of the function that refreshes the supplier names and risk names with their ids. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is 0 */30 * * * *.\n\n\t r. **LogLevel** - DEBUG, INFO or ERROR. Default is INFO.\n\n4. Once all application settings have been entered, click **Save**." + } + ] +} diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/README.md b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/README.md new file mode 100644 index 00000000000..46cce5735e1 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/README.md @@ -0,0 +1,159 @@ +# Censys Supplier Risk Data Connector +* [Introduction](#Introduction) +* [Description](#Description) +* [Folders](#Folders) +* [Prerequisites](#Prerequisites) +* [Configuration](#Configuration) +* [Installing for the users](#Installing-for-the-users) +* [Installing for testing](#Installing-for-testing) + +## Introduction + +This folder contains the Azure function time trigger code for Censys Supplier Risk Data Connector. The connector will run periodically and ingest the Censys risk events into the Microsoft Sentinel logs custom table `CensysRiskEvents_CL`. + +## Description + +The [Censys](https://censys.com/) Supplier Risk Data Connector brings the risk events of the Censys Supplier Risk change feed (`/v1/supplychains/default/risk-events`) into Microsoft Sentinel. Each event is one state change (opened, resolved or reopened) of a risk on an asset of one of your suppliers. + +* The events are sent to the table `CensysRiskEvents_CL` with the [Logs Ingestion API](https://learn.microsoft.com/azure/azure-monitor/logs/logs-ingestion-api-overview), through a data collection endpoint and a data collection rule that the deployment template creates. +* The connector saves the position of the last ingested event after every page, so each run continues where the previous one stopped. +* Suppliers and risks can be selected by name. A second function keeps the supplier names and risk names, with their ids, in storage tables of the Function App (nothing from it is sent to Microsoft Sentinel). +* The solution parsers `CensysRiskEvents` (one row per event) and `CensysRisks` (the current state of each risk) read the table. + +## Folders + +1. `CensysRiskEventsFunctionConnector/` - This contains the package, requirements, ARM JSON file, connector page template JSON, and other dependencies. +2. `CensysSRRiskEvents/` - This contains the Azure function source code to ingest the data of below mentioned endpoint. + * Risk events (`/v1/supplychains/default/risk-events`) +3. `CensysSRSuppliersAndRisks/` - This contains the Azure function source code that keeps the names and ids of the suppliers and risks in storage tables, to be used by the `SupplierNames` and `RiskNames` inputs. It reads the below mentioned endpoints. + * Suppliers (`/v1/supplychains/default/suppliers`) + * Risks (`/v1/supplychains/default/risks`) +4. `SharedCode/` - This contains the constants, logger, exceptions, Censys API client, checkpoint, input validation, supplier and risk lookup and Logs Ingestion code used in each azure function. + +## Prerequisites +1. A Censys personal access token and the Censys Organization ID are required. Users on Starter, Search, and Core plans need the **API Access** role. See the documentation to [create a token](https://accounts.censys.io/settings/personal-access-tokens). +2. A Microsoft Entra application (app registration) with a client secret is required. The deployment template gives it the **Monitoring Metrics Publisher** role on the data collection rule. +3. Read and write permissions on the Log Analytics workspace of Microsoft Sentinel, and permission to create a Function App and role assignments in its resource group. + +The Function App runs on the Flex Consumption plan (Linux, Python 3.14). Azure Government is not supported. + +## Configuration + +### STEP 1 - Steps to Create/Get a Censys Personal Access Token and Organization ID + +Follow these instructions to get the values needed by this connector: + +1. Sign in to Censys and open [Personal access tokens](https://accounts.censys.io/settings/personal-access-tokens). +2. Create a personal access token. Users on Starter, Search, and Core plans need the **API Access** role. +3. Copy the token. It is shown only once. +4. Copy the **Organization ID** (UUID) of the Censys organization you want to collect risk events for. + +Supplier names and risk names, used by the optional filters, can be picked from the Censys Supplier Risk UI. + +### STEP 2 - Get the name of the Log Analytics workspace + +The deployment template creates the table, the data collection endpoint and the data collection rule in the workspace of Microsoft Sentinel, so deploy it in the resource group of that workspace. You need the **name** of the workspace (workspace Overview page). + +### STEP 3 - App Registration steps for the Application in Microsoft Entra ID + +This integration requires an App registration in the Azure portal. Follow the steps in this section to create a new application in Microsoft Entra ID: + +1. Sign in to the Azure portal. +2. Search for and select Microsoft Entra ID. +3. Under Manage, select App registrations > New registration. +4. Enter a display Name for your application. +5. Select Register to complete the initial app registration. +6. When registration finishes, the Azure portal displays the app registration's Overview pane. You see the Application (client) ID and Tenant ID. The client ID and Tenant ID are required as configuration parameters for the execution of Censys Supplier Risk Data Connector. +- Reference link: https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app + +### STEP 4 - Add a client secret for application in Microsoft Entra ID + +Sometimes called an application password, a client secret is a string value required for the execution of Censys Supplier Risk Data Connector. Follow the steps in this section to create a new Client Secret: + +1. In the Azure portal, in App registrations, select your application. +2. Select Certificates & secrets > Client secrets > New client secret. +3. Add a description for your client secret. +4. Select an expiration for the secret or specify a custom lifetime. Limit is 24 months. +5. Select Add. +6. Record the secret's value for use in your client application code. This secret value is never displayed again after you leave this page. The secret value is required as configuration parameter for the execution of Censys Supplier Risk Data Connector. +- Reference link: https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app#add-a-client-secret + +### STEP 5 - Get Object ID of your application in Microsoft Entra ID + +After creating your app registration, follow the steps in this section to get the Object ID: + +1. Go to Microsoft Entra ID. +2. Select Enterprise applications from the left menu. +3. Find your newly created application in the list (you can search by the name you provided). +4. Click on the application. +5. On the overview page, copy the Object ID. This is the **Azure_Entra_Object_Id** needed for the ARM template role assignment. The template gives this application the **Monitoring Metrics Publisher** role on the data collection rule, so no other role needs to be assigned. + +## Installing for the users + +After the solution is published, we can find the connector in the connector gallery of Microsoft Sentinel among other connectors in Data connectors section of Microsoft Sentinel. + +i. Go to Microsoft Sentinel -> Data Connectors + +ii. Click on the Censys Supplier Risk (using Azure Functions), connector page will open. + +iii. Click on Deploy to Azure + +[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-CensysSRRiskEventsFunction-azuredeploy) + +It will lead to a custom deployment page where user need to select **Subscription** and **Resource Group** (use the resource group of the Log Analytics workspace of Microsoft Sentinel). +And need to enter below information to configure Censys Supplier Risk Data Connector. + +User Inputs | Default Value +------------- | ------------- +Censys SR Function Name | CensysSR (name prefix, up to 10 characters) +Censys Api Key | None +Censys Organization Id | None +Censys Start Date Time | Empty (all events Censys still retains) +Censys Supplier Names | Empty (all suppliers) +Censys Risk Names | Empty (all risks) +Censys Change Types | Empty (opened, resolved and reopened) +Censys Severity | Empty (all severities) +Censys Rapid Response | Empty (both) +Azure_Client_Id | None +Azure_Client_Secret | None +Azure_Tenant_Id | None +Azure_Entra_Object_Id | None +WorkspaceName | None +Location | Location of the resource group +Censys Risk Events Table Name | CensysRiskEvents_CL +Censys Risk Events Collection Schedule | 0 */10 * * * * +Censys Supplier and Risk List Refresh Schedule | 0 */30 * * * * +AppInsightsWorkspaceResourceID | None +Censys SR Function Log Level | INFO + +* **Censys Start Date Time:** UTC time to start collecting from, for example `2026-10-01T00:00:00Z`. It must not be in the future. +* **Censys Supplier Names / Censys Risk Names:** names separated by an asterisk (*), for example `OpenAI*Harvey*Acme, Inc.` or `Log4Shell*Exposed Redis`. Pick the names from the Censys Supplier Risk UI. Names are not case-sensitive and the maximum is 500 characters. A name that is not found is skipped with a warning in the log. If none of the names of a setting is found, nothing is collected in that run and the next run tries again. +* **Censys Change Types:** comma-separated `opened`, `resolved`, `reopened`. **Censys Severity:** comma-separated `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`. **Censys Rapid Response:** `true` for rapid response findings only, `false` to exclude them. +* A wrong value in an input stops the run with an error that names the input, and nothing is sent. A change of a filter applies from the next run and continues from the current position, so avoid changing the filters often. +* **Warning:** keep the frequency of both schedules at least 10 minutes for a better experience. The schedules are NCRONTAB expressions with six fields (second, minute, hour, day, month, day of week). + +The template creates the table `CensysRiskEvents_CL`, a data collection endpoint, a data collection rule, the Application Insights component, the storage account and the Function App. + +The connector should start ingesting the data into the logs at every time interval specified in Censys Risk Events Collection Schedule during configuration. The function that refreshes the supplier and risk lists runs once when the app starts and then at every time interval specified in Censys Supplier and Risk List Refresh Schedule. + +## Installing for testing + + +i. Log in to Azure portal using the URL - [Azure Portal-Home](https://ms.portal.azure.com/?feature.BringYourOwnConnector=true&feature.experimentationflights=ConnectorsKO#home). + +ii. Go to Microsoft Sentinel -> Data Connectors + +iii. Click the “import” button at the top and select the json file `CensysSR_RiskEvents_FunctionApp.json` downloaded on your local machine from Github. + +iv. This will load the connector page and rest of the process will be same as the Installing for users guideline above. + + +Each invocation and its logs of the function can be seen in Function App service of Azure, available in the Azure Portal outside the Microsoft Sentinel. + +i. Go to Function App and click on the function which you have deployed, identified with the given name at the deployment stage. + +ii. Go to Functions -> `CensysSRRiskEvents` or `CensysSRSuppliersAndRisks` -> Monitor + +iii. By clicking on invocation time, you can see all the logs for that run. The log lines start with `CensysSR`. Set the Censys SR Function Log Level to `DEBUG` for more detail. + +**Note: Furthermore we can check logs in Application Insights of the given function in detail if needed. We can search the logs by operation ID in Transaction search section.** diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/__init__.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/__init__.py new file mode 100644 index 00000000000..cbd24bb0822 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/__init__.py @@ -0,0 +1 @@ +"""Shared code of the Censys Supplier Risk functions.""" diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/censys_client.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/censys_client.py new file mode 100644 index 00000000000..f7697a09d8f --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/censys_client.py @@ -0,0 +1,165 @@ +"""Client for the Censys supply chain API (risk events and suppliers).""" +import time +from typing import Dict, Iterator, List, Optional + +import requests + +from . import consts +from .exceptions import CensysAuthError, CensysCursorExpired, CensysException, CensysTimeBudgetReached +from .logger import applogger + + +class CensysClient: + """Reads risk events and suppliers page by page, following the Censys cursor rules.""" + + def __init__(self, api_token: str, organization_id: str, deadline: Optional[float] = None, + hard_deadline: Optional[float] = None, session: Optional[requests.Session] = None): + """Create the client with a bearer token. + + deadline: no API call is started at or after this time (epoch seconds). + hard_deadline: no API call is allowed to run past this time. + """ + self.deadline = deadline + self.hard_deadline = hard_deadline + self.organization_id = organization_id + self.session = session or requests.Session() + self.session.headers.update({ + "Authorization": f"Bearer {api_token}", + "Accept": "application/json", + "User-Agent": f"{consts.USER_AGENT_PRODUCT}/{consts.CONNECTOR_VERSION}", + }) + + @staticmethod + def _request_headers() -> Dict[str, str]: + """Headers that change with every request: the current epoch time in seconds.""" + return {"ts": str(int(time.time()))} + + def _check_budget(self) -> None: + """Stop the run when the time budget is used up, so that no new API call is started.""" + if self.deadline is not None and time.time() >= self.deadline: + raise CensysTimeBudgetReached("Time budget reached, no further Censys API call is started.") + + def _wait(self, seconds: float) -> None: + """Sleep, but never past the deadline.""" + if self.deadline is not None: + seconds = min(seconds, max(0.0, self.deadline - time.time())) + time.sleep(seconds) + + def _timeout(self) -> float: + """Return the timeout of one request, which never lets the request run past the hard limit.""" + if self.hard_deadline is None: + return consts.REQUEST_TIMEOUT + return max(1.0, min(consts.REQUEST_TIMEOUT, self.hard_deadline - time.time())) + + def get_page(self, params: Dict[str, str], path: Optional[str] = None) -> dict: + """GET one page, retrying throttling and transient server errors within the time budget.""" + path = path or consts.RISK_EVENTS_PATH + url = consts.API_BASE_URL + path + for attempt in range(1, consts.MAX_RETRIES + 1): + self._check_budget() + try: + resp = self.session.get(url, params=params, headers=self._request_headers(), + timeout=self._timeout()) + except requests.RequestException as err: + applogger.warning(f"{consts.LOG_PREFIX} get_page: request error (attempt {attempt}): {err}") + self._wait(min(2 ** attempt, 60)) + continue + status = resp.status_code + if status == 200: + return self._read_response(resp, path, params) + if status in (401, 403): + raise CensysAuthError(f"Censys API rejected the request (HTTP {status}). " + f"Check the CensysApiToken and OrganizationId settings: {resp.text[:300]}") + if status == 404: + raise CensysException("Censys returned 404: the organization has no default supply chain. " + "Check the OrganizationId setting.") + if status == 410: + raise CensysCursorExpired("The checkpoint is older than the Censys retention window (HTTP 410).") + if status in (429, 500, 502, 503, 504): + try: + wait = int(resp.headers.get("Retry-After", "")) + except ValueError: + wait = 2 ** attempt + applogger.warning(f"{consts.LOG_PREFIX} get_page: HTTP {status}, " + f"retrying in {min(wait, 60)}s (attempt {attempt})") + self._wait(min(wait, 60)) + continue + raise CensysException(f"Censys API error (HTTP {status}): {resp.text[:300]}") + raise CensysException(f"Censys API still failing after {consts.MAX_RETRIES} attempts.") + + @staticmethod + def _read_response(resp: requests.Response, path: str, params: Dict[str, str]) -> dict: + """Parse a successful response and log a short summary of the call.""" + try: + body = resp.json() + except ValueError: + raise CensysException(f"Censys returned HTTP 200 with a body that is not valid JSON: {resp.text[:200]}") + result = (body or {}).get("result") or {} + key = {consts.RISK_EVENTS_PATH: "risk_events", consts.RISKS_PATH: "risks"}.get(path, "suppliers") + count = len(result.get(key) or []) + has_next = "yes" if result.get("next_page_token") else "no" + applogger.info(f"{consts.LOG_PREFIX} get_page: API call GET {path} -> HTTP 200, " + f"{count} {key.replace('_', ' ')}, next_page_token={has_next}") + shown = {k: ("" if k == "page_token" else v) for k, v in params.items()} + applogger.debug(f"{consts.LOG_PREFIX} get_page: request parameters {shown}") + return body + + def iter_pages(self, inserted_after_time: str, inserted_after_event_id: Optional[str], + filters: Dict[str, str]) -> Iterator[List[dict]]: + """Yield each page of events, oldest first, until the last page (no next_page_token). + + Every request of one read keeps the same inserted_after_time and inserted_after_event_id, as the API + requires when page_token is used. + """ + base = { + "organization_id": self.organization_id, + "page_size": consts.PAGE_SIZE, + "inserted_after_time": inserted_after_time, + } + if inserted_after_event_id: + base["inserted_after_event_id"] = inserted_after_event_id + base.update(filters) + page_token = None + while True: + params = dict(base) + if page_token: + params["page_token"] = page_token + result = (self.get_page(params) or {}).get("result") or {} + events = result.get("risk_events") or [] + if events: + yield events + page_token = result.get("next_page_token") + if not page_token: + return + + def iter_suppliers(self) -> Iterator[List[dict]]: + """Yield each page of suppliers of the default supply chain until the last page (no next_page_token).""" + base = { + "organization_id": self.organization_id, + "page_size": consts.SUPPLIERS_PAGE_SIZE, + "order_by": "created_at_asc", + } + return self._iter_list(base, consts.SUPPLIERS_PATH, "suppliers") + + def iter_risks(self) -> Iterator[List[dict]]: + """Yield each page of risks of the default supply chain until the last page (no next_page_token).""" + base = {"organization_id": self.organization_id, "page_size": consts.RISKS_PAGE_SIZE} + return self._iter_list(base, consts.RISKS_PATH, "risks") + + def _iter_list(self, base: Dict[str, str], path: str, key: str) -> Iterator[List[dict]]: + """Yield the pages of a list endpoint. key is the result field holding the items.""" + page_token, seen = None, set() + while True: + params = dict(base) + if page_token: + params["page_token"] = page_token + result = (self.get_page(params, path) or {}).get("result") or {} + items = result.get(key) or [] + if items: + yield items + page_token = result.get("next_page_token") + if not page_token: + return + if page_token in seen: + raise CensysException(f"Censys returned the same {key} page token twice, stopping to avoid a loop.") + seen.add(page_token) diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/checkpoint.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/checkpoint.py new file mode 100644 index 00000000000..40461ff73cf --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/checkpoint.py @@ -0,0 +1,62 @@ +"""Checkpoint of the last ingested risk event, stored in an Azure Storage Table.""" +import datetime +import hashlib +from typing import Optional, Tuple + +from azure.core.exceptions import ResourceNotFoundError +from azure.data.tables import TableServiceClient, UpdateMode + +from .consts import CHECKPOINT_PARTITION_KEY, LOG_PREFIX +from .exceptions import CensysException +from .logger import applogger + + +def checkpoint_key(*parts: str) -> str: + """Build a stable row key from the settings that define what is collected. + + A different organization or filter set (change types, severity, rapid response, risk type IDs) gets its own + checkpoint, so changing one of them never skips events. The suppliers are not part of the key: changing the + supplier names continues from the current checkpoint. + """ + return hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()[:32] + + +class CheckpointManager: + """Reads and writes the (inserted_at, event_id) checkpoint.""" + + def __init__(self, connection_string: str, table_name: str): + """Create the table client, creating the table when needed.""" + self.table_name = table_name + try: + service = TableServiceClient.from_connection_string(conn_str=connection_string) + self.table = service.create_table_if_not_exists(table_name) + except Exception as err: + applogger.error(f"{LOG_PREFIX} CheckpointManager.__init__: error accessing checkpoint table " + f"{table_name}: {err}") + raise CensysException(f"Error accessing checkpoint table {table_name}: {err}") + + def get(self, row_key: str) -> Optional[Tuple[str, str]]: + """Return (inserted_at, event_id) or None when no checkpoint exists.""" + try: + entity = self.table.get_entity(partition_key=CHECKPOINT_PARTITION_KEY, row_key=row_key) + return entity.get("InsertedAt"), entity.get("EventId") + except ResourceNotFoundError: + return None + except Exception as err: + applogger.error(f"{LOG_PREFIX} CheckpointManager.get: error reading checkpoint: {err}") + raise CensysException(f"Error reading checkpoint: {err}") + + def set(self, row_key: str, inserted_at: str, event_id: str) -> None: + """Store the inserted_at and id of the last event that was ingested.""" + entity = { + "PartitionKey": CHECKPOINT_PARTITION_KEY, + "RowKey": row_key, + "InsertedAt": inserted_at, + "EventId": event_id, + "UpdatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), + } + try: + self.table.upsert_entity(mode=UpdateMode.REPLACE, entity=entity) + except Exception as err: + applogger.error(f"{LOG_PREFIX} CheckpointManager.set: error saving checkpoint: {err}") + raise CensysException(f"Error saving checkpoint: {err}") diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/consts.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/consts.py new file mode 100644 index 00000000000..16e99565fe7 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/consts.py @@ -0,0 +1,57 @@ +"""Constants and configuration read from the Function App settings.""" +import os + +API_BASE_URL = "https://api.platform.censys.io" +RISK_EVENTS_PATH = "/v1/supplychains/default/risk-events" +SUPPLIERS_PATH = "/v1/supplychains/default/suppliers" +SUPPLIERS_PAGE_SIZE = 100 +RISKS_PATH = "/v1/supplychains/default/risks" +RISKS_PAGE_SIZE = 100 +CONNECTOR_VERSION = "1.0.0" +USER_AGENT_PRODUCT = "CensysSRMSSentinel" + +# Names in SupplierNames and RiskNames are separated by an asterisk, so a name can contain a comma. At most +# MAX_SUPPLIER_IDS ids are sent in one request: the supplier_ids parameter is part of the URL, and the API rejected +# URLs of about 16000 characters (400 ids) while it accepted about 8000 characters (200 ids). +SUPPLIER_NAME_SEPARATOR = "*" +MAX_SUPPLIER_IDS = 200 +MAX_SUPPLIER_NAMES_LENGTH = 500 +MAX_RISK_NAMES_LENGTH = 500 +MAX_RISK_TYPE_IDS = 100 + +PAGE_SIZE = 1000 +REQUEST_TIMEOUT = 60 +MAX_RETRIES = 5 +DEFAULT_START_TIME = "1970-01-01T00:00:00Z" +RETENTION_DAYS = 29 +# No Censys API call starts after MAX_RUN_SECONDS, and no call may run past HARD_LIMIT_SECONDS, so the run always +# ends before the 10 minute function timeout. Progress is checkpointed after every page. +MAX_RUN_SECONDS = 540 +HARD_LIMIT_SECONDS = 580 + +API_TOKEN = os.environ.get("CensysApiToken") +ORGANIZATION_ID = os.environ.get("OrganizationId") +INITIAL_START_TIME = os.environ.get("InitialStartTime", "") +SUPPLIER_NAMES = os.environ.get("SupplierNames", "") +RISK_NAMES = os.environ.get("RiskNames", "") +CHANGE_TYPES = os.environ.get("ChangeTypes", "") +SEVERITY = os.environ.get("Severity", "") +RAPID_RESPONSE = os.environ.get("RapidResponse", "") + +ALLOWED_CHANGE_TYPES = ("opened", "resolved", "reopened") +ALLOWED_SEVERITIES = ("LOW", "MEDIUM", "HIGH", "CRITICAL") + +CONN_STRING = os.environ.get("AzureWebJobsStorage") +CHECKPOINT_TABLE_NAME = os.environ.get("CheckpointTableName", "CensysSRRiskEventsCheckpoint") +CHECKPOINT_PARTITION_KEY = "CensysSRRiskEvents" +SUPPLIERS_TABLE_NAME = os.environ.get("SuppliersTableName", "CensysSRSuppliers") +RISKS_TABLE_NAME = os.environ.get("RisksTableName", "CensysSRRisks") + +DATA_COLLECTION_ENDPOINT = os.environ.get("AZURE_DATA_COLLECTION_ENDPOINT") +DATA_COLLECTION_RULE_ID = os.environ.get("AZURE_DATA_COLLECTION_RULE_ID") +STREAM_NAME = os.environ.get("StreamName", "Custom-CensysRiskEvents_CL") +SCOPE = os.environ.get("SCOPE", "https://monitor.azure.com/.default") + +# Top-level event fields declared in the data collection rule stream +EVENT_FIELDS = ("id", "inserted_at", "occurred_at", "change_type", "risk_instance") +LOG_PREFIX = "CensysSR" diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/exceptions.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/exceptions.py new file mode 100644 index 00000000000..6df0bf21010 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/exceptions.py @@ -0,0 +1,17 @@ +"""Exceptions raised by the Censys risk events connector.""" + + +class CensysException(Exception): + """Generic connector error.""" + + +class CensysAuthError(CensysException): + """The Censys API rejected the credentials or the organization.""" + + +class CensysCursorExpired(CensysException): + """The checkpoint is older than the Censys event retention window (HTTP 410).""" + + +class CensysTimeBudgetReached(CensysException): + """The run used its time budget. Everything ingested so far is already checkpointed.""" diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/logger.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/logger.py new file mode 100644 index 00000000000..c96fe151acc --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/logger.py @@ -0,0 +1,15 @@ +"""Logger of the connector. The log level comes from the LogLevel setting.""" +import logging +import os +import sys + +log_level = (os.environ.get("LogLevel") or "INFO").upper() + +applogger = logging.getLogger("azure") +if log_level in ("DEBUG", "INFO", "WARNING", "ERROR"): + applogger.setLevel(getattr(logging, log_level)) +else: + applogger.setLevel(logging.INFO) +if not any(isinstance(h, logging.StreamHandler) and getattr(h, "stream", None) is sys.stdout + for h in applogger.handlers): + applogger.addHandler(logging.StreamHandler(stream=sys.stdout)) diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/sentinel.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/sentinel.py new file mode 100644 index 00000000000..b82c70a186d --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/sentinel.py @@ -0,0 +1,44 @@ +"""Send events to Microsoft Sentinel through the Logs Ingestion API and the data collection rule.""" +from azure.core.exceptions import ClientAuthenticationError, HttpResponseError +from azure.identity import AzureAuthorityHosts, DefaultAzureCredential +from azure.monitor.ingestion import LogsIngestionClient + +from . import consts +from .exceptions import CensysException +from .logger import applogger + + +class SentinelIngestor: + """Uploads risk events to the data collection rule stream.""" + + def __init__(self, endpoint: str, rule_id: str, stream_name: str, scope: str): + """Create the ingestion client. + + DefaultAzureCredential uses the Microsoft Entra app set in the AZURE_CLIENT_ID, AZURE_CLIENT_SECRET and + AZURE_TENANT_ID settings. + """ + self.rule_id = rule_id + self.stream_name = stream_name + if ".us" in scope: + credential = DefaultAzureCredential(authority=AzureAuthorityHosts.AZURE_GOVERNMENT) + else: + credential = DefaultAzureCredential() + self.client = LogsIngestionClient(endpoint, credential=credential, credential_scopes=[scope]) + + def upload(self, events: list) -> None: + """Upload one page of events. + + Raises an error when any part of the upload fails, so the checkpoint does not move. + """ + logs = [{field: event.get(field) for field in consts.EVENT_FIELDS} for event in events] + try: + self.client.upload(rule_id=self.rule_id, stream_name=self.stream_name, logs=logs) + except ClientAuthenticationError as err: + applogger.error(f"{consts.LOG_PREFIX} upload: authentication error while uploading to Sentinel: {err}") + raise CensysException(f"Authentication error while uploading to Sentinel: {err}") + except HttpResponseError as err: + applogger.error(f"{consts.LOG_PREFIX} upload: HTTP error while uploading to Sentinel: {err}") + raise CensysException(f"HTTP error while uploading to Sentinel: {err}") + except Exception as err: + applogger.error(f"{consts.LOG_PREFIX} upload: unexpected error while uploading to Sentinel: {err}") + raise CensysException(f"Unexpected error while uploading to Sentinel: {err}") diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/suppliers.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/suppliers.py new file mode 100644 index 00000000000..950d87b9d94 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/suppliers.py @@ -0,0 +1,113 @@ +"""Supplier and risk names with their ids, kept in Azure Storage Tables so that names can be used as an input.""" +from typing import Dict, Iterable, List, Tuple + +from azure.core.exceptions import ResourceNotFoundError +from azure.data.tables import TableServiceClient, UpdateMode + +from . import consts +from .exceptions import CensysException +from .logger import applogger + +_BATCH = 100 # entities per Table transaction, all in the same partition + + +def normalize(name: str) -> str: + """Compare names without caring about case or extra spaces. Used at run time only, nothing normalized is stored.""" + return " ".join((name or "").split()).casefold() + + +def parse_names(raw: str, placeholder: str = "suppliernames") -> List[str]: + """Split a names setting (SupplierNames or RiskNames) on the separator, trim, drop empty entries and duplicates.""" + names, seen = [], set() + for part in (raw or "").replace("\n", consts.SUPPLIER_NAME_SEPARATOR).split(consts.SUPPLIER_NAME_SEPARATOR): + name = " ".join(part.split()) + key = normalize(name) + if key and key != placeholder and key not in seen: + seen.add(key) + names.append(name) + return names + + +def resolve(names: Iterable[str], suppliers: Iterable[Tuple[str, str]]) -> Tuple[List[str], List[str]]: + """Return (ids, names that matched nothing). suppliers is a list of (id, display name), suppliers or risks. + + A name can match several entries, and all their ids are returned. The ids keep the order of the names. + """ + by_name: Dict[str, List[str]] = {} + for supplier_id, display_name in suppliers: + by_name.setdefault(normalize(display_name), []).append(supplier_id) + ids, missing = [], [] + for name in names: + found = by_name.get(normalize(name)) + if not found: + missing.append(name) + continue + for supplier_id in sorted(found): + if supplier_id not in ids: + ids.append(supplier_id) + return ids, missing + + +class SupplierStore: + """Table with one row per supplier: partition = organization id, row = supplier id, DisplayName as returned. + + RiskStore uses the same layout for risks, whose name field is called name. + """ + + name_field = "display_name" + + def __init__(self, connection_string: str, table_name: str, organization_id: str): + """Create the table client, creating the table when needed.""" + self.organization_id = organization_id + try: + service = TableServiceClient.from_connection_string(conn_str=connection_string) + self.table = service.create_table_if_not_exists(table_name) + except Exception as err: + applogger.error(f"{consts.LOG_PREFIX} SupplierStore.__init__: error accessing suppliers table " + f"{table_name}: {err}") + raise CensysException(f"Error accessing suppliers table {table_name}: {err}") + + def load(self) -> List[Tuple[str, str]]: + """Return (supplier id, display name) of every stored supplier of the organization.""" + try: + rows = self.table.query_entities(f"PartitionKey eq '{self.organization_id}'") + return [(row["RowKey"], row.get("DisplayName") or "") for row in rows] + except ResourceNotFoundError: + return [] + except Exception as err: + applogger.error(f"{consts.LOG_PREFIX} SupplierStore.load: error reading suppliers table: {err}") + raise CensysException(f"Error reading suppliers table: {err}") + + def sync(self, pages: Iterable[List[dict]]) -> Tuple[int, int]: + """Store every supplier of the pages, then remove the rows of suppliers that no longer exist in Censys. + + Returns (suppliers stored, rows removed). Rows are removed only after all pages were read and written, + so a failed run never removes anything. + """ + latest: Dict[str, str] = {} + for page in pages: + for supplier in page: + if supplier.get("id"): + latest[supplier["id"]] = supplier.get(self.name_field) or "" + try: + self._write([("upsert", {"PartitionKey": self.organization_id, "RowKey": sid, "DisplayName": name}, + {"mode": UpdateMode.REPLACE}) for sid, name in latest.items()]) + stale = [sid for sid, _ in self.load() if sid not in latest] + self._write([("delete", {"PartitionKey": self.organization_id, "RowKey": sid}) for sid in stale]) + except CensysException: + raise + except Exception as err: + applogger.error(f"{consts.LOG_PREFIX} SupplierStore.sync: error writing suppliers table: {err}") + raise CensysException(f"Error writing suppliers table: {err}") + return len(latest), len(stale) + + def _write(self, operations: list) -> None: + """Send the operations to the table in transactions of at most 100 entities.""" + for start in range(0, len(operations), _BATCH): + self.table.submit_transaction(operations[start:start + _BATCH]) + + +class RiskStore(SupplierStore): + """Table with one row per risk: partition = organization id, row = risk id, DisplayName = risk name.""" + + name_field = "name" diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/validation.py b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/validation.py new file mode 100644 index 00000000000..06547f71976 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/SharedCode/validation.py @@ -0,0 +1,186 @@ +"""Validation of the Function App settings. + +Every problem is logged with the name of the setting, then the run stops with one error. +""" +import datetime +import re +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Tuple + +from . import consts +from .exceptions import CensysException +from .logger import applogger +from .suppliers import parse_names + +_UUID = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") +_RFC3339 = re.compile(r"^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?([Zz]|[+-]\d{2}:\d{2})$") +_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" +_START_TIME_EXAMPLE = "2026-10-01T00:00:00Z" + + +@dataclass +class Settings: + """The validated and cleaned settings of the main function.""" + + api_token: str + organization_id: str + storage_connection: str + data_collection_endpoint: str + data_collection_rule_id: str + start_time: str + supplier_names: List[str] = field(default_factory=list) + risk_names: List[str] = field(default_factory=list) + change_types: List[str] = field(default_factory=list) + severity: List[str] = field(default_factory=list) + rapid_response: Optional[str] = None + + def filters(self, supplier_ids: Optional[List[str]] = None, + risk_type_ids: Optional[List[str]] = None) -> Dict[str, str]: + """Return the query parameters of the filters. A filter without a value is never included.""" + filters: Dict[str, str] = {} + for name, values in (("supplier_ids", supplier_ids), ("risk_type_ids", risk_type_ids), + ("change_types", self.change_types), ("severity", self.severity)): + if values: + filters[name] = ",".join(values) + if self.rapid_response: + filters["rapid_response"] = self.rapid_response + return filters + + +class _Problems: + """The settings that failed the validation. Each problem is logged when it is added.""" + + def __init__(self): + """Start with no problems.""" + self.settings: List[str] = [] + + def add(self, setting: str, message: str) -> None: + """Log the problem with the name of the setting and remember the setting.""" + applogger.error(f"{consts.LOG_PREFIX} validate_settings: setting '{setting}' {message}") + self.settings.append(setting) + + def raise_if_any(self) -> None: + """Stop the run with one error that lists all the settings with a problem.""" + if self.settings: + raise CensysException(f"Invalid settings: {', '.join(self.settings)}. " + "See the previous error logs for the details.") + + +def _value(raw: Optional[str], name: str) -> str: + """Trimmed value. The name of the setting itself (a placeholder left in by mistake) counts as empty.""" + value = (raw or "").strip() + return "" if value.lower() == name.lower() else value + + +def _items(raw: str, separator: str = ",") -> List[str]: + """Split a list setting on the separator, trim each item, and drop empty items and duplicates.""" + items = [] + for item in raw.replace("\n", separator).split(separator): + item = item.strip() + if item and item not in items: + items.append(item) + return items + + +def parse_start_time(raw: Optional[str]) -> Tuple[Optional[str], Optional[str]]: + """Return (start time in UTC as yyyy-MM-ddTHH:mm:ssZ, error). Empty means all events Censys still retains.""" + value = _value(raw, "InitialStartTime") + if not value: + return consts.DEFAULT_START_TIME, None + match = _RFC3339.match(value) + if not match: + return None, (f"must be a UTC time with date and time, for example {_START_TIME_EXAMPLE}, " + f"but is '{value}'.") + year, month, day, hour, minute, second = (int(match.group(i)) for i in range(1, 7)) + micro = int((match.group(7) or "0")[:6].ljust(6, "0")) + zone = match.group(8) + try: + offset = datetime.timedelta(0) + if zone not in ("Z", "z"): + hours, minutes = int(zone[1:3]), int(zone[4:6]) + offset = datetime.timedelta(hours=hours, minutes=minutes) * (1 if zone[0] == "+" else -1) + moment = datetime.datetime(year, month, day, hour, minute, second, micro, tzinfo=datetime.timezone(offset)) + except ValueError: + return None, f"is not a real date and time: '{value}'. Use the form {_START_TIME_EXAMPLE}." + moment = moment.astimezone(datetime.timezone.utc) + if moment > datetime.datetime.now(datetime.timezone.utc): + return None, f"must not be in the future: '{value}'." + return moment.strftime(_TIME_FORMAT), None + + +def _choice_list(raw: str, name: str, allowed: Tuple[str, ...], upper: bool, problems: _Problems) -> List[str]: + """Return the values of a list setting that only accepts the allowed values, and report the other values.""" + values = [] + for item in _items(_value(raw, name)): + item = item.upper() if upper else item.lower() + if item not in values: + values.append(item) + bad = [v for v in values if v not in allowed] + if bad: + problems.add(name, f"has unsupported values {bad}. Allowed values: {', '.join(allowed)}.") + return values + + +def load_common(problems: Optional[_Problems] = None) -> Tuple[str, str, str]: + """Validate and return (API token, organization id, storage connection string), used by both functions.""" + own = problems is None + problems = problems or _Problems() + token = (consts.API_TOKEN or "").strip() + org = (consts.ORGANIZATION_ID or "").strip() + storage = (consts.CONN_STRING or "").strip() + if not token: + problems.add("CensysApiToken", "is not set. Enter your Censys personal access token.") + if not org: + problems.add("OrganizationId", "is not set. Enter the UUID of your Censys organization.") + elif not _UUID.match(org): + problems.add("OrganizationId", + f"must be a UUID, for example 00000000-0000-0000-0000-000000000000, but is '{org}'.") + if not storage: + problems.add("AzureWebJobsStorage", "is not set.") + if own: + problems.raise_if_any() + return token, org, storage + + +def load_settings() -> Settings: + """Validate every setting of the main function and return the cleaned values. + + Raises one CensysException that lists all the settings with a problem. + """ + problems = _Problems() + token, org, storage = load_common(problems) + endpoint = (consts.DATA_COLLECTION_ENDPOINT or "").strip() + rule = (consts.DATA_COLLECTION_RULE_ID or "").strip() + if not endpoint: + problems.add("AZURE_DATA_COLLECTION_ENDPOINT", "is not set.") + if not rule: + problems.add("AZURE_DATA_COLLECTION_RULE_ID", "is not set.") + + start_time, error = parse_start_time(consts.INITIAL_START_TIME) + if error: + problems.add("InitialStartTime", error) + + change_types = _choice_list(consts.CHANGE_TYPES, "ChangeTypes", consts.ALLOWED_CHANGE_TYPES, False, problems) + severity = _choice_list(consts.SEVERITY, "Severity", consts.ALLOWED_SEVERITIES, True, problems) + rapid = _value(consts.RAPID_RESPONSE, "RapidResponse").lower() + if rapid and rapid not in ("true", "false"): + problems.add("RapidResponse", f"must be true or false, but is '{rapid}'. Leave it empty for both.") + rapid = "" + + raw_risks = (consts.RISK_NAMES or "").strip() + if len(raw_risks) > consts.MAX_RISK_NAMES_LENGTH: + problems.add("RiskNames", + f"has {len(raw_risks)} characters, at most {consts.MAX_RISK_NAMES_LENGTH} are allowed.") + risk_names = parse_names(raw_risks, "risknames") + + raw_names = (consts.SUPPLIER_NAMES or "").strip() + if len(raw_names) > consts.MAX_SUPPLIER_NAMES_LENGTH: + problems.add("SupplierNames", + f"has {len(raw_names)} characters, at most {consts.MAX_SUPPLIER_NAMES_LENGTH} are allowed.") + supplier_names = parse_names(raw_names) + + problems.raise_if_any() + return Settings(api_token=token, organization_id=org, storage_connection=storage, + data_collection_endpoint=endpoint, data_collection_rule_id=rule, start_time=start_time, + supplier_names=supplier_names, risk_names=risk_names, change_types=change_types, + severity=severity, rapid_response=rapid or None) diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/azuredeploy_CensysSR_RiskEvents_FunctionApp.json b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/azuredeploy_CensysSR_RiskEvents_FunctionApp.json new file mode 100644 index 00000000000..540e8eb2b23 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/azuredeploy_CensysSR_RiskEvents_FunctionApp.json @@ -0,0 +1,650 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "Censys SR Function Name": { + "type": "string", + "defaultValue": "CensysSR", + "metadata": { + "description": "Name prefix of the Function App to create (up to 10 characters, letters and digits)." + }, + "minLength": 1, + "maxLength": 10 + }, + "Censys Api Key": { + "type": "securestring", + "metadata": { + "description": "Censys personal access token. Starter, Search and Core plans need the API Access role." + } + }, + "Censys Organization Id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "metadata": { + "description": "UUID of the Censys organization to collect risk events for, for example 00000000-0000-0000-0000-000000000000." + } + }, + "Censys Start Date Time": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. UTC time to start collecting from, in the form 2026-10-01T00:00:00Z. It must not be in the future. Leave empty for full history (all events Censys still retains)." + } + }, + "Censys Supplier Names": { + "type": "string", + "defaultValue": "", + "maxLength": 500, + "metadata": { + "description": "Optional. Names of the suppliers to collect risk events for, separated by an asterisk (*). For example: OpenAI*Harvey*Acme, Inc. Names are not case-sensitive and can be picked from the Censys Supplier Risk UI. Names that are not found among your suppliers are skipped with a warning. Leave empty to collect all suppliers. A change applies from the next run and continues from the current position, so avoid changing it often. Maximum 500 characters." + } + }, + "Censys Risk Names": { + "type": "string", + "defaultValue": "", + "maxLength": 500, + "metadata": { + "description": "Optional. Names of the risks to collect risk events for, separated by an asterisk (*). For example: Log4Shell*Exposed Redis. Names are not case-sensitive and must match the risk name shown in the Censys Supplier Risk UI. Names that are not found among the risks of your supply chain are skipped with a warning. Leave empty to collect all risks. A change applies from the next run and continues from the current position, so avoid changing it often. Maximum 500 characters." + } + }, + "Censys Change Types": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Comma-separated change types: opened, resolved, reopened. Other values stop the run with an error. Leave empty for all." + } + }, + "Censys Severity": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Comma-separated severities: CRITICAL, HIGH, MEDIUM, LOW. Other values stop the run with an error. Leave empty for all." + } + }, + "Censys Rapid Response": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. true for rapid response findings only, false to exclude them. Leave empty for both." + }, + "allowedValues": [ + "", + "true", + "false" + ] + }, + "Azure_Client_Id": { + "type": "string", + "metadata": { + "description": "Enter the Azure client ID of the Microsoft Entra app that ingests the data. It is the app you created for this connector." + } + }, + "Azure_Client_Secret": { + "type": "securestring", + "metadata": { + "description": "Enter the client secret that you created for that Microsoft Entra app." + } + }, + "Azure_Tenant_Id": { + "type": "string", + "metadata": { + "description": "Enter the Azure tenant ID of your Microsoft Entra ID." + } + }, + "Azure_Entra_Object_Id": { + "type": "string", + "metadata": { + "description": "Enter the object ID of your Microsoft Entra app (Enterprise application). It gets the Monitoring Metrics Publisher role on the data collection rule." + } + }, + "WorkspaceName": { + "type": "string", + "metadata": { + "description": "Name of the Log Analytics workspace of Microsoft Sentinel. Deploy this template in the resource group of that workspace. The template creates the table CensysRiskEvents_CL, a data collection endpoint and a data collection rule in it." + } + }, + "Location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Region of the data collection endpoint and rule. It must be the region of the workspace." + } + }, + "Censys Risk Events Table Name": { + "type": "string", + "defaultValue": "CensysRiskEvents_CL", + "minLength": 4, + "maxLength": 52, + "metadata": { + "description": "Name of the table in the workspace that the connector writes the risk events to. The suffix _CL is added, or corrected, when it is missing or in another case. Maximum 52 characters. Warning: the default is CensysRiskEvents_CL, and the parser CensysRiskEvents, the workbook, the analytic rule and the connector status check of this solution read that table. If you use another name, update these resources to read the new table, otherwise they show no data." + } + }, + "Censys Risk Events Collection Schedule": { + "type": "string", + "defaultValue": "0 */10 * * * *", + "metadata": { + "description": "Quartz (NCRONTAB) cron expression for how often the function runs, for example 0 */10 * * * *. Warning: keep the frequency at least 10 minutes for a better experience. Default is every 10 minutes." + } + }, + "Censys Supplier and Risk List Refresh Schedule": { + "type": "string", + "defaultValue": "0 */30 * * * *", + "metadata": { + "description": "Quartz (NCRONTAB) cron expression for how often the function that refreshes the lists of supplier names and risk names with their ids runs, for example 0 */30 * * * *. It also runs once when the app starts. Warning: keep the frequency at least 10 minutes for a better experience. Default is every 30 minutes." + } + }, + "AppInsightsWorkspaceResourceID": { + "type": "string", + "metadata": { + "description": "Resource ID of the Log Analytics workspace used by Application Insights. Use 'Log Analytics Workspace, Properties' blade, Resource ID." + } + }, + "Censys SR Function Log Level": { + "type": "string", + "defaultValue": "INFO", + "metadata": { + "description": "Log level of the functions: DEBUG, INFO or ERROR." + }, + "allowedValues": [ + "DEBUG", + "INFO", + "ERROR" + ] + } + }, + "variables": { + "FunctionName": "[concat(toLower(parameters('Censys SR Function Name')), uniqueString(resourceGroup().id))]", + "deploymentStorageContainerName": "[concat('app-package-', toLower(parameters('Censys SR Function Name')))]", + "storageBlobDataOwnerRoleId": "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", + "monitoringMetricsPublisherRoleId": "3913510d-42f4-4e42-8a64-420c390055eb", + "scope": "[concat(replace(environment().portal, 'portal', 'monitor'), '/.default')]", + "suffix": "[uniqueString(resourceGroup().id)]", + "dceName": "[concat('CensysSRRiskEventsDCE', variables('suffix'))]", + "dcrName": "[concat('CensysSRRiskEventsDCR', variables('suffix'))]", + "dcrId": "[resourceId('Microsoft.Insights/dataCollectionRules', variables('dcrName'))]", + "dceId": "[resourceId('Microsoft.Insights/dataCollectionEndpoints', variables('dceName'))]", + "tableName": "[if(endsWith(toLower(parameters('Censys Risk Events Table Name')), '_cl'), concat(substring(parameters('Censys Risk Events Table Name'), 0, sub(length(parameters('Censys Risk Events Table Name')), 3)), '_CL'), concat(parameters('Censys Risk Events Table Name'), '_CL'))]", + "streamName": "[concat('Custom-', variables('tableName'))]", + "workspaceResId": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('WorkspaceName'))]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces/tables", + "apiVersion": "2022-10-01", + "name": "[concat(parameters('WorkspaceName'), '/', variables('tableName'))]", + "properties": { + "plan": "Analytics", + "schema": { + "name": "[variables('tableName')]", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "EventId", + "type": "string" + }, + { + "name": "InsertedAt", + "type": "datetime" + }, + { + "name": "OccurredAt", + "type": "datetime" + }, + { + "name": "ChangeType", + "type": "string" + }, + { + "name": "RiskInstanceId", + "type": "string" + }, + { + "name": "SupplyChainId", + "type": "string" + }, + { + "name": "AssetType", + "type": "string" + }, + { + "name": "RiskTypeId", + "type": "string" + }, + { + "name": "CveId", + "type": "string" + }, + { + "name": "RiskCategory", + "type": "string" + }, + { + "name": "RiskName", + "type": "string" + }, + { + "name": "Description", + "type": "string" + }, + { + "name": "Remediations", + "type": "dynamic" + }, + { + "name": "Severity", + "type": "string" + }, + { + "name": "Score", + "type": "real" + }, + { + "name": "Confidence", + "type": "real" + }, + { + "name": "Status", + "type": "string" + }, + { + "name": "ResolveTime", + "type": "datetime" + }, + { + "name": "ResolveReason", + "type": "string" + }, + { + "name": "SupplierId", + "type": "string" + }, + { + "name": "SupplierDisplayName", + "type": "string" + }, + { + "name": "Suppliers", + "type": "dynamic" + }, + { + "name": "AssetIp", + "type": "string" + }, + { + "name": "AssetHostname", + "type": "string" + }, + { + "name": "AssetPort", + "type": "int" + }, + { + "name": "ServiceTransportProtocol", + "type": "string" + }, + { + "name": "ServicePort", + "type": "int" + }, + { + "name": "ServiceProtocol", + "type": "string" + }, + { + "name": "Tags", + "type": "dynamic" + }, + { + "name": "RapidResponse", + "type": "bool" + }, + { + "name": "Evidence", + "type": "string" + }, + { + "name": "FirstDetectedAt", + "type": "datetime" + }, + { + "name": "LastDetectedAt", + "type": "datetime" + }, + { + "name": "CreatedAt", + "type": "datetime" + }, + { + "name": "PlatformUrl", + "type": "string" + } + ] + } + } + }, + { + "type": "Microsoft.Insights/dataCollectionEndpoints", + "apiVersion": "2022-06-01", + "name": "[variables('dceName')]", + "location": "[parameters('Location')]", + "kind": "Linux", + "properties": { + "description": "Data collection endpoint of the Censys SR function connector", + "networkAcls": { + "publicNetworkAccess": "Enabled" + } + } + }, + { + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2023-03-11", + "name": "[variables('dcrName')]", + "location": "[parameters('Location')]", + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces/tables', parameters('WorkspaceName'), variables('tableName'))]", + "[variables('dceId')]" + ], + "properties": { + "dataCollectionEndpointId": "[variables('dceId')]", + "streamDeclarations": { + "[variables('streamName')]": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "inserted_at", + "type": "datetime" + }, + { + "name": "occurred_at", + "type": "datetime" + }, + { + "name": "change_type", + "type": "string" + }, + { + "name": "risk_instance", + "type": "dynamic" + } + ] + } + }, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "[variables('workspaceResId')]", + "name": "clv2ws1" + } + ] + }, + "dataFlows": [ + { + "streams": [ + "[variables('streamName')]" + ], + "destinations": [ + "clv2ws1" + ], + "transformKql": "source | extend ri = todynamic(risk_instance) | extend TimeGenerated = todatetime(occurred_at), EventId = tostring(id), InsertedAt = todatetime(inserted_at), OccurredAt = todatetime(occurred_at), ChangeType = tostring(change_type), RiskInstanceId = tostring(ri.id), SupplyChainId = tostring(ri.supply_chain_id), AssetType = tostring(ri.asset_type), RiskTypeId = tostring(ri.risk_type_id), CveId = iff(tostring(ri.risk_type_id) matches regex @'^CVE-[0-9]{4}-[0-9]+$', tostring(ri.risk_type_id), ''), RiskCategory = tostring(ri.risk_category), RiskName = tostring(ri.risk_name), Description = tostring(ri.description), Remediations = todynamic(ri.remediations), Severity = tostring(ri.severity), Score = toreal(ri.score), Confidence = toreal(ri.confidence), Status = tostring(ri.status), ResolveTime = todatetime(ri.resolve_time), ResolveReason = tostring(ri.resolve_reason), SupplierId = tostring(ri.suppliers[0].id), SupplierDisplayName = tostring(ri.suppliers[0].display_name), Suppliers = todynamic(ri.suppliers), AssetIp = tostring(ri.host.ip), AssetHostname = tostring(ri.web.hostname), AssetPort = toint(ri.web.port), ServiceTransportProtocol = tostring(ri.service.transport_protocol), ServicePort = toint(ri.service.port), ServiceProtocol = tostring(ri.service.protocol), Tags = todynamic(ri.tags), RapidResponse = tobool(ri.rapid_response), Evidence = tostring(ri.evidence), FirstDetectedAt = todatetime(ri.first_detected_at), LastDetectedAt = todatetime(ri.last_detected_at), CreatedAt = todatetime(ri.created_at), PlatformUrl = tostring(ri.platform_url) | project TimeGenerated, EventId, InsertedAt, OccurredAt, ChangeType, RiskInstanceId, SupplyChainId, AssetType, RiskTypeId, CveId, RiskCategory, RiskName, Description, Remediations, Severity, Score, Confidence, Status, ResolveTime, ResolveReason, SupplierId, SupplierDisplayName, Suppliers, AssetIp, AssetHostname, AssetPort, ServiceTransportProtocol, ServicePort, ServiceProtocol, Tags, RapidResponse, Evidence, FirstDetectedAt, LastDetectedAt, CreatedAt, PlatformUrl", + "outputStream": "[variables('streamName')]" + } + ] + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('FunctionName')]", + "location": "[resourceGroup().location]", + "kind": "web", + "properties": { + "Application_Type": "web", + "ApplicationId": "[variables('FunctionName')]", + "WorkspaceResourceId": "[parameters('AppInsightsWorkspaceResourceID')]" + } + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-01-01", + "name": "[tolower(variables('FunctionName'))]", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "kind": "StorageV2", + "properties": { + "networkAcls": { + "bypass": "AzureServices", + "virtualNetworkRules": [], + "ipRules": [], + "defaultAction": "Allow" + }, + "supportsHttpsTrafficOnly": true, + "minimumTlsVersion": "TLS1_2", + "encryption": { + "services": { + "file": { + "keyType": "Account", + "enabled": true + }, + "blob": { + "keyType": "Account", + "enabled": true + } + }, + "keySource": "Microsoft.Storage" + } + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2023-01-01", + "name": "[concat(tolower(variables('FunctionName')), '/default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName')))]" + ], + "properties": { + "cors": { + "corsRules": [] + }, + "deleteRetentionPolicy": { + "enabled": false + } + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[concat(tolower(variables('FunctionName')), '/default/', variables('deploymentStorageContainerName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', tolower(variables('FunctionName')), 'default')]" + ], + "properties": { + "publicAccess": "None" + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[concat(tolower(variables('FunctionName')), '/default/', 'azure-webjobs-hosts')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', tolower(variables('FunctionName')), 'default')]" + ], + "properties": { + "publicAccess": "None" + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[concat(tolower(variables('FunctionName')), '/default/', 'azure-webjobs-secrets')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', tolower(variables('FunctionName')), 'default')]" + ], + "properties": { + "publicAccess": "None" + } + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2023-12-01", + "name": "[variables('FunctionName')]", + "location": "[resourceGroup().location]", + "kind": "functionapp", + "sku": { + "tier": "FlexConsumption", + "name": "FC1" + }, + "properties": { + "reserved": true + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2023-12-01", + "name": "[variables('FunctionName')]", + "location": "[resourceGroup().location]", + "kind": "functionapp,linux", + "identity": { + "type": "SystemAssigned" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('FunctionName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName')))]", + "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', tolower(variables('FunctionName')), 'default', variables('deploymentStorageContainerName'))]", + "[resourceId('Microsoft.Insights/components', variables('FunctionName'))]" + ], + "properties": { + "name": "[variables('FunctionName')]", + "httpsOnly": true, + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('FunctionName'))]", + "functionAppConfig": { + "deployment": { + "storage": { + "type": "blobContainer", + "value": "[concat(reference(resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName')))).primaryEndpoints.blob, variables('deploymentStorageContainerName'))]", + "authentication": { + "type": "SystemAssignedIdentity" + } + } + }, + "scaleAndConcurrency": { + "maximumInstanceCount": 40, + "instanceMemoryMB": 2048 + }, + "runtime": { + "name": "python", + "version": "3.14" + } + } + }, + "resources": [ + { + "apiVersion": "2023-12-01", + "type": "config", + "name": "appsettings", + "dependsOn": [ + "[concat('Microsoft.Web/sites/', variables('FunctionName'))]", + "[variables('dcrId')]", + "[variables('dceId')]" + ], + "properties": { + "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionName')), '2015-05-01').InstrumentationKey]", + "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('FunctionName')), '2015-05-01').ConnectionString]", + "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', tolower(variables('FunctionName')), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName'))), '2023-01-01').keys[0].value, ';EndpointSuffix=', environment().suffixes.storage)]", + "CensysApiToken": "[parameters('Censys Api Key')]", + "OrganizationId": "[parameters('Censys Organization Id')]", + "InitialStartTime": "[parameters('Censys Start Date Time')]", + "SupplierNames": "[parameters('Censys Supplier Names')]", + "RiskNames": "[parameters('Censys Risk Names')]", + "ChangeTypes": "[parameters('Censys Change Types')]", + "Severity": "[parameters('Censys Severity')]", + "RapidResponse": "[parameters('Censys Rapid Response')]", + "Schedule": "[parameters('Censys Risk Events Collection Schedule')]", + "SuppliersAndRisksSchedule": "[parameters('Censys Supplier and Risk List Refresh Schedule')]", + "LogLevel": "[parameters('Censys SR Function Log Level')]", + "StreamName": "[variables('streamName')]", + "SCOPE": "[variables('scope')]", + "AZURE_DATA_COLLECTION_ENDPOINT": "[reference(variables('dceId'), '2022-06-01').logsIngestion.endpoint]", + "AZURE_DATA_COLLECTION_RULE_ID": "[reference(variables('dcrId'), '2023-03-11').immutableId]", + "AZURE_CLIENT_ID": "[parameters('Azure_Client_Id')]", + "AZURE_CLIENT_SECRET": "[parameters('Azure_Client_Secret')]", + "AZURE_TENANT_ID": "[parameters('Azure_Tenant_Id')]" + } + } + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName'))), variables('storageBlobDataOwnerRoleId'), variables('FunctionName'))]", + "scope": "[concat('Microsoft.Storage/storageAccounts', '/', tolower(variables('FunctionName')))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName')))]", + "[resourceId('Microsoft.Web/sites', variables('FunctionName'))]" + ], + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('storageBlobDataOwnerRoleId'))]", + "principalType": "ServicePrincipal", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('FunctionName')), '2023-12-01', 'Full').identity.principalId]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(variables('dcrId'), variables('monitoringMetricsPublisherRoleId'), parameters('Azure_Entra_Object_Id'))]", + "scope": "[variables('dcrId')]", + "dependsOn": [ + "[variables('dcrId')]" + ], + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('monitoringMetricsPublisherRoleId'))]", + "principalType": "ServicePrincipal", + "principalId": "[parameters('Azure_Entra_Object_Id')]" + } + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "WaitSection", + "location": "[resourceGroup().location]", + "kind": "AzurePowerShell", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('FunctionName'))]", + "[guid(resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName'))), variables('storageBlobDataOwnerRoleId'), variables('FunctionName'))]" + ], + "properties": { + "azPowerShellVersion": "7.0", + "scriptContent": "start-sleep -Seconds 30", + "cleanupPreference": "Always", + "retentionInterval": "PT1H" + } + }, + { + "type": "Microsoft.Web/sites/extensions", + "apiVersion": "2022-09-01", + "name": "[format('{0}/{1}', variables('FunctionName'), 'onedeploy')]", + "dependsOn": [ + "WaitSection" + ], + "properties": { + "packageUri": "https://aka.ms/sentinel-CensysSR100-functionapp", + "remoteBuild": true + } + } + ], + "outputs": { + "FunctionAppName": { + "type": "string", + "value": "[variables('FunctionName')]" + } + } +} diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/host.json b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/host.json new file mode 100644 index 00000000000..3b13d3477a1 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/host.json @@ -0,0 +1,22 @@ +{ + "version": "2.0", + "logging": { + "logLevel": { + "default": "Information", + "Host.Results": "Information", + "Function": "Information", + "Host.Aggregator": "Information" + }, + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "functionTimeout": "00:10:00", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} diff --git a/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/requirements.txt b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/requirements.txt new file mode 100644 index 00000000000..24845b7a8a8 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data Connectors/CensysRiskEventsFunctionConnector/requirements.txt @@ -0,0 +1,9 @@ +# DO NOT include azure-functions-worker in this file +# The Python Worker is managed by Azure Functions platform +# Manually managing azure-functions-worker may cause unexpected issues + +azure-functions +requests +azure-identity +azure-monitor-ingestion +azure-data-tables diff --git a/Solutions/Censys Supplier Risk/Data/Solution_Censys.json b/Solutions/Censys Supplier Risk/Data/Solution_Censys.json new file mode 100644 index 00000000000..8a3dfebe9dc --- /dev/null +++ b/Solutions/Censys Supplier Risk/Data/Solution_Censys.json @@ -0,0 +1,24 @@ +{ + "Name": "Censys Supplier Risk", + "Author": "Censys - support@censys.com", + "Logo": "", + "Description": "Bring Censys Supplier Risk into Microsoft Sentinel. The solution collects supply chain risk events (risks opened, reopened and resolved for your suppliers) from the Censys platform, and provides a parser, a workbook and an analytic rule that raises incidents for new and reopened supplier risks and keeps resolved events with the incident of the same risk.", + "Data Connectors": [ + "Data Connectors/CensysRiskEventsFunctionConnector/CensysSR_RiskEvents_FunctionApp.json" + ], + "Parsers": [ + "Parsers/CensysRiskEvents.yaml", + "Parsers/CensysRisks.yaml" + ], + "Workbooks": [ + "Workbooks/CensysSupplierRisk.json" + ], + "Analytic Rules": [ + "Analytic Rules/CensysRiskStateChange.yaml" + ], + "BasePath": "C:\\GitHub\\Azure-Sentinel\\Solutions\\Censys Supplier Risk", + "Version": "3.0.0", + "Metadata": "SolutionMetadata.json", + "TemplateSpec": true, + "Is1PConnector": false +} diff --git a/Solutions/Censys Supplier Risk/Package/3.0.0.zip b/Solutions/Censys Supplier Risk/Package/3.0.0.zip new file mode 100644 index 00000000000..663945f25ac Binary files /dev/null and b/Solutions/Censys Supplier Risk/Package/3.0.0.zip differ diff --git a/Solutions/Censys Supplier Risk/Package/createUiDefinition.json b/Solutions/Censys Supplier Risk/Package/createUiDefinition.json new file mode 100644 index 00000000000..31144cf1a14 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Package/createUiDefinition.json @@ -0,0 +1,176 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "isWizard": false, + "basics": { + "description": "\n\n**Note:** Please refer to the following before installing the solution: \n\n• Review the solution [Release Notes](https://github.com/Azure/Azure-Sentinel/tree/master/Solutions/Censys%20Supplier%20Risk/ReleaseNotes.md)\n\n • There may be [known issues](https://aka.ms/sentinelsolutionsknownissues) pertaining to this Solution, please refer to them before installing.\n\nBring Censys Supplier Risk into Microsoft Sentinel. The solution collects supply chain risk events (risks opened, reopened and resolved for your suppliers) from the Censys platform, and provides a parser, a workbook and an analytic rule that raises incidents for new and reopened supplier risks and keeps resolved events with the incident of the same risk.\n\n**Data Connectors:** 1, **Parsers:** 2, **Workbooks:** 1, **Analytic Rules:** 1\n\n[Learn more about Microsoft Sentinel](https://aka.ms/azuresentinel) | [Learn more about Solutions](https://aka.ms/azuresentinelsolutionsdoc)", + "subscription": { + "resourceProviders": [ + "Microsoft.OperationsManagement/solutions", + "Microsoft.OperationalInsights/workspaces/providers/alertRules", + "Microsoft.Insights/workbooks", + "Microsoft.Logic/workflows" + ] + }, + "location": { + "metadata": { + "hidden": "Hiding location, we get it from the log analytics workspace" + }, + "visible": false + }, + "resourceGroup": { + "allowExisting": true + } + } + }, + "basics": [ + { + "name": "getLAWorkspace", + "type": "Microsoft.Solutions.ArmApiControl", + "toolTip": "This filters by workspaces that exist in the Resource Group selected", + "condition": "[greater(length(resourceGroup().name),0)]", + "request": { + "method": "GET", + "path": "[concat(subscription().id,'/providers/Microsoft.OperationalInsights/workspaces?api-version=2020-08-01')]" + } + }, + { + "name": "workspace", + "type": "Microsoft.Common.DropDown", + "label": "Workspace", + "placeholder": "Select a workspace", + "toolTip": "This dropdown will list only workspace that exists in the Resource Group selected", + "constraints": { + "allowedValues": "[map(filter(basics('getLAWorkspace').value, (filter) => contains(toLower(filter.id), toLower(resourceGroup().name))), (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.name, '\"}')))]", + "required": true + }, + "visible": true + } + ], + "steps": [ + { + "name": "dataconnectors", + "label": "Data Connectors", + "bladeTitle": "Data Connectors", + "elements": [ + { + "name": "dataconnectors1-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "This Solution installs the data connector for Censys Supplier Risk. You can get Censys Supplier Risk custom log data in your Microsoft Sentinel workspace. After installing the solution, configure and enable this data connector by following guidance in Manage solution view." + } + }, + { + "name": "dataconnectors-parser-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "The Solution installs a parser that transforms the ingested data into Microsoft Sentinel normalized format. The normalized format enables better correlation of different types of data from different data sources to drive end-to-end outcomes seamlessly in security monitoring, hunting, incident investigation and response scenarios in Microsoft Sentinel." + } + }, + { + "name": "dataconnectors-link1", + "type": "Microsoft.Common.TextBlock", + "options": { + "link": { + "label": "Learn more about connecting data sources", + "uri": "https://docs.microsoft.com/azure/sentinel/connect-data-sources" + } + } + } + ] + }, + { + "name": "workbooks", + "label": "Workbooks", + "subLabel": { + "preValidation": "Configure the workbooks", + "postValidation": "Done" + }, + "bladeTitle": "Workbooks", + "elements": [ + { + "name": "workbooks-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "This solution installs workbook(s) to help you gain insights into the telemetry collected in Microsoft Sentinel. After installing the solution, start using the workbook in Manage solution view." + } + }, + { + "name": "workbooks-link", + "type": "Microsoft.Common.TextBlock", + "options": { + "link": { + "label": "Learn more", + "uri": "https://docs.microsoft.com/azure/sentinel/tutorial-monitor-your-data" + } + } + }, + { + "name": "workbook1", + "type": "Microsoft.Common.Section", + "label": "Censys Supplier Risk", + "elements": [ + { + "name": "workbook1-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Gain insight into supplier risk collected from Censys Supplier Risk: open risks by severity, risk events by category, opened and closed risks over time, top risks by open risk count, supplier risk ranking and rapid response events, with details one selection away." + } + } + ] + } + ] + }, + { + "name": "analytics", + "label": "Analytics", + "subLabel": { + "preValidation": "Configure the analytics", + "postValidation": "Done" + }, + "bladeTitle": "Analytics", + "elements": [ + { + "name": "analytics-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "This solution installs the following analytic rule templates. After installing the solution, create and enable analytic rules in Manage solution view." + } + }, + { + "name": "analytics-link", + "type": "Microsoft.Common.TextBlock", + "options": { + "link": { + "label": "Learn more", + "uri": "https://docs.microsoft.com/azure/sentinel/tutorial-detect-threats-custom?WT.mc_id=Portal-Microsoft_Azure_CreateUIDef" + } + } + }, + { + "name": "analytic1", + "type": "Microsoft.Common.Section", + "label": "Censys SR - Risk State Change", + "elements": [ + { + "name": "analytic1-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Creates one incident per supplier risk instance that is opened, or reopened, with CRITICAL or HIGH severity. Alerts are grouped by RiskInstanceId, so further events of the same risk instance within 24 hours are added to the same incident while it is open.\nAlert severity follows the Censys severity: CRITICAL and HIGH map to High, MEDIUM to Medium and LOW to Low. Resolved events follow the same mapping, and any other severity is Informational.\nBy default the rule alerts only on new CRITICAL and HIGH risks. The query holds commented lines to follow the whole life of a risk instead (reopened events of any severity and resolved events), see the comments in the query." + } + } + ] + } + ] + } + ], + "outputs": { + "workspace-location": "[first(map(filter(basics('getLAWorkspace').value, (filter) => and(contains(toLower(filter.id), toLower(resourceGroup().name)),equals(filter.name,basics('workspace')))), (item) => item.location))]", + "location": "[location()]", + "workspace": "[basics('workspace')]" + } + } +} diff --git a/Solutions/Censys Supplier Risk/Package/mainTemplate.json b/Solutions/Censys Supplier Risk/Package/mainTemplate.json new file mode 100644 index 00000000000..f24fefa6115 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Package/mainTemplate.json @@ -0,0 +1,1026 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "author": "Censys - support@censys.com", + "comments": "Solution template for Censys Supplier Risk" + }, + "parameters": { + "location": { + "type": "string", + "minLength": 1, + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Not used, but needed to pass arm-ttk test `Location-Should-Not-Be-Hardcoded`. We instead use the `workspace-location` which is derived from the LA workspace" + } + }, + "workspace-location": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "[concat('Region to deploy solution resources -- separate from location selection',parameters('location'))]" + } + }, + "workspace": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" + } + }, + "workbook1-name": { + "type": "string", + "defaultValue": "Censys Supplier Risk", + "minLength": 1, + "metadata": { + "description": "Name for the workbook" + } + } + }, + "variables": { + "email": "support@censys.com", + "_email": "[variables('email')]", + "_solutionName": "Censys Supplier Risk", + "_solutionVersion": "3.0.0", + "solutionId": "censys.censys_supplier_risk", + "_solutionId": "[variables('solutionId')]", + "uiConfigId1": "CensysSRRiskEventsFunction", + "_uiConfigId1": "[variables('uiConfigId1')]", + "dataConnectorContentId1": "CensysSRRiskEventsFunction", + "_dataConnectorContentId1": "[variables('dataConnectorContentId1')]", + "dataConnectorId1": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId1'))]", + "_dataConnectorId1": "[variables('dataConnectorId1')]", + "dataConnectorTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentId1'))))]", + "dataConnectorVersion1": "1.0.0", + "_dataConnectorcontentProductId1": "[concat(take(variables('_solutionId'),50),'-','dc','-', uniqueString(concat(variables('_solutionId'),'-','DataConnector','-',variables('_dataConnectorContentId1'),'-', variables('dataConnectorVersion1'))))]", + "parserObject1": { + "_parserName1": "[concat(parameters('workspace'),'/','CensysRiskEvents')]", + "_parserId1": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'CensysRiskEvents')]", + "parserTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-pr-',uniquestring('CensysRiskEvents-Parser')))]", + "parserVersion1": "1.0.0", + "parserContentId1": "CensysRiskEvents-Parser" + }, + "parserObject2": { + "_parserName2": "[concat(parameters('workspace'),'/','CensysRisks')]", + "_parserId2": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'CensysRisks')]", + "parserTemplateSpecName2": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-pr-',uniquestring('CensysRisks-Parser')))]", + "parserVersion2": "1.0.0", + "parserContentId2": "CensysRisks-Parser" + }, + "workbookVersion1": "1.0.0", + "workbookContentId1": "CensysSupplierRiskWorkbook", + "workbookId1": "[resourceId('Microsoft.Insights/workbooks', variables('workbookContentId1'))]", + "workbookTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-wb-',uniquestring(variables('_workbookContentId1'))))]", + "_workbookContentId1": "[variables('workbookContentId1')]", + "workspaceResourceId": "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]", + "_workbookcontentProductId1": "[concat(take(variables('_solutionId'),50),'-','wb','-', uniqueString(concat(variables('_solutionId'),'-','Workbook','-',variables('_workbookContentId1'),'-', variables('workbookVersion1'))))]", + "analyticRuleObject1": { + "analyticRuleVersion1": "1.0.0", + "_analyticRulecontentId1": "8a208922-5ef7-4721-ad8b-7f69e2163d79", + "analyticRuleId1": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '8a208922-5ef7-4721-ad8b-7f69e2163d79')]", + "analyticRuleTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('8a208922-5ef7-4721-ad8b-7f69e2163d79')))]", + "_analyticRulecontentProductId1": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','8a208922-5ef7-4721-ad8b-7f69e2163d79','-', '1.0.0')))]" + }, + "_solutioncontentProductId": "[concat(take(variables('_solutionId'),50),'-','sl','-', uniqueString(concat(variables('_solutionId'),'-','Solution','-',variables('_solutionId'),'-', variables('_solutionVersion'))))]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('dataConnectorTemplateSpecName1')]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "Censys Supplier Risk data connector with template version 3.0.0", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorVersion1')]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId1'))]", + "apiVersion": "2021-03-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "GenericUI", + "properties": { + "connectorUiConfig": { + "id": "[variables('_uiConfigId1')]", + "title": "Censys Supplier Risk (using Azure Functions)", + "publisher": "Censys", + "descriptionMarkdown": "The Censys Supplier Risk data connector brings the Censys risk-events change feed (`/v1/supplychains/default/risk-events`) into Microsoft Sentinel using Azure Functions and the [Logs Ingestion API](https://learn.microsoft.com/azure/azure-monitor/logs/logs-ingestion-api-overview). The function runs on a schedule and remembers the `inserted_at` and `id` of the last event it ingested, so each run continues where the previous one stopped. Events are written to the table `CensysRiskEvents_CL` through a data collection endpoint and a data collection rule that the deployment template creates. Suppliers and risks can be selected by name; the connector keeps the supplier names and risk names with their ids in storage tables.", + "graphQueries": [ + { + "metricName": "Total Censys risk events received", + "legend": "CensysRiskEvents_CL", + "baseQuery": "CensysRiskEvents_CL" + } + ], + "sampleQueries": [ + { + "description": "All Censys risk events received, newest first.", + "query": "CensysRiskEvents_CL\n| sort by TimeGenerated desc" + }, + { + "description": "Newly opened or reopened risks with critical or high severity.", + "query": "CensysRiskEvents_CL\n| where ChangeType in ('opened', 'reopened')\n| where Severity in ('CRITICAL', 'HIGH')\n| project TimeGenerated, Severity, RiskName, SupplierDisplayName, AssetIp, AssetHostname, PlatformUrl\n| order by TimeGenerated desc" + }, + { + "description": "Rapid response findings (actively exploited).", + "query": "CensysRiskEvents_CL\n| where RapidResponse == true and ChangeType in ('opened', 'reopened')\n| project TimeGenerated, Severity, CveId, RiskName, SupplierDisplayName, AssetIp, Evidence, PlatformUrl" + }, + { + "description": "Currently open risks per supplier (latest event per risk).", + "query": "CensysRiskEvents_CL\n| summarize arg_max(InsertedAt, *) by RiskInstanceId\n| where Status == 'open'\n| summarize OpenRisks = count() by SupplierDisplayName, Severity\n| order by OpenRisks desc" + }, + { + "description": "Risk events mapped to known CVEs.", + "query": "CensysRiskEvents_CL\n| where isnotempty(CveId)\n| summarize AffectedAssets = dcount(RiskInstanceId) by CveId, Severity\n| order by AffectedAssets desc" + } + ], + "dataTypes": [ + { + "name": "CensysRiskEvents_CL", + "lastDataReceivedQuery": "CensysRiskEvents_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + } + ], + "connectivityCriterias": [ + { + "type": "IsConnectedQuery", + "value": [ + "CensysRiskEvents_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + } + ], + "availability": { + "status": 1, + "isPreview": false + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "read and write permissions on the workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ], + "customs": [ + { + "name": "Microsoft.Web/sites permissions", + "description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)." + }, + { + "name": "REST API Credentials/permissions", + "description": "A Censys personal access token and the Censys Organization ID are required. Users on Starter, Search, and Core plans need the API Access role. [Create a token](https://accounts.censys.io/settings/personal-access-tokens)." + }, + { + "name": "Microsoft Entra application", + "description": "An app registration with a client secret is required. Permission to create role assignments on the data collection rule (Owner or User Access Administrator) is required to give the application the Monitoring Metrics Publisher role." + } + ] + }, + "instructionSteps": [ + { + "description": ">**NOTE:** This connector uses Azure Functions to connect to the Censys API to pull risk events into Microsoft Sentinel using the [Logs Ingestion API](https://learn.microsoft.com/azure/azure-monitor/logs/logs-ingestion-api-overview). The deployment template creates the table CensysRiskEvents_CL, a data collection endpoint and a data collection rule in the workspace. A second function, CensysSRSuppliersAndRisks, keeps the lists of supplier names and risk names, with their ids, in storage tables of the Function App (nothing is sent to Microsoft Sentinel), so supplier names and risk names can be used as an input. This might result in additional data ingestion costs. Check the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/) for details." + }, + { + "description": ">**(Optional Step)** Securely store the Censys API token and the client secret in Azure Key Vault. Azure Key Vault provides a secure mechanism to store and retrieve key values. [Follow these instructions](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) to use Azure Key Vault with an Azure Function App." + }, + { + "description": "**STEP 1 - Steps to Create/Get a Censys Personal Access Token and Organization ID**\n\n Follow these instructions to get the values needed by this connector:\n 1. Sign in to Censys and open [Personal access tokens](https://accounts.censys.io/settings/personal-access-tokens).\n 2. Create a personal access token. Users on Starter, Search, and Core plans need the **API Access** role.\n 3. Copy the token. It is shown only once.\n 4. Copy the **Organization ID** (UUID) of the Censys organization you want to collect risk events for." + }, + { + "description": "**STEP 2 - Get the name of the Log Analytics workspace**\n\n The deployment template creates the table, the data collection endpoint and the data collection rule in the workspace of Microsoft Sentinel, so deploy it in the resource group of that workspace. You need the **name** of the workspace (workspace Overview page)." + }, + { + "description": "**STEP 3 - App Registration steps for the Application in Microsoft Entra ID**\n\n This integration requires an App registration in the Azure portal. Follow the steps in this section to create a new application in Microsoft Entra ID:\n 1. Sign in to the [Azure portal](https://portal.azure.com/).\n 2. Search for and select **Microsoft Entra ID**.\n 3. Under **Manage**, select **App registrations > New registration**.\n 4. Enter a display **Name** for your application.\n 5. Select **Register** to complete the initial app registration.\n 6. When registration finishes, the Azure portal displays the app registration's Overview pane. You see the **Application (client) ID** and **Tenant ID**. The client ID and Tenant ID are required as configuration parameters for the execution of the Censys SR data connector. \n\n> **Reference link:** [https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app)" + }, + { + "description": "**STEP 4 - Add a client secret for application in Microsoft Entra ID**\n\n Sometimes called an application password, a client secret is a string value required for the execution of the Censys SR data connector. Follow the steps in this section to create a new Client Secret:\n 1. In the Azure portal, in **App registrations**, select your application.\n 2. Select **Certificates & secrets > Client secrets > New client secret**.\n 3. Add a description for your client secret.\n 4. Select an expiration for the secret or specify a custom lifetime. Limit is 24 months.\n 5. Select **Add**. \n 6. *Record the secret's value for use in your client application code. This secret value is never displayed again after you leave this page.* The secret value is required as configuration parameter for the execution of the Censys SR data connector. \n\n> **Reference link:** [https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app#add-a-client-secret](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app#add-a-client-secret)" + }, + { + "description": "**STEP 5 - Get Object ID of your application in Microsoft Entra ID**\n\n After creating your app registration, follow the steps in this section to get Object ID:\n 1. Go to **Microsoft Entra ID**.\n 2. Select **Enterprise applications** from the left menu.\n 3. Find your newly created application in the list (you can search by the name you provided).\n 4. Click on the application.\n 5. On the overview page, copy the **Object ID**. This is the **Azure_Entra_Object_Id** needed for your ARM template role assignment. The template gives this application the **Monitoring Metrics Publisher** role on the data collection rule, so no other role needs to be assigned." + }, + { + "description": "**STEP 6 - Choose ONE from the following two deployment options to deploy the connector and the associated Azure Function**\n\n>**IMPORTANT:** Before deploying the Censys SR data connector, have the Censys API token, the organization ID, the workspace name and the Azure credentials (client ID, client secret, tenant ID, object ID) ready." + }, + { + "description": "Use this method for automated deployment of the Censys SR connector.\n\n1. Click the **Deploy to Azure** button below. \n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-CensysSRRiskEventsFunction-azuredeploy)\n2. Select the preferred **Subscription** and **Resource Group**. Use the resource group of the Log Analytics workspace of Microsoft Sentinel. \n3. Enter the below information : \n\n\t a. **Censys SR Function Name** - Name prefix of the Azure Function App to be created, up to 10 characters. Default is CensysSR.\n\n\t b. **Censys Api Key** - Enter your Censys personal access token.\n\n\t c. **Censys Organization Id** - Enter the UUID of the Censys organization to collect risk events for, for example 00000000-0000-0000-0000-000000000000.\n\n\t d. **Censys Start Date Time** - Optional. UTC time to start collecting from, in the form 2026-10-01T00:00:00Z. It must not be in the future. Leave empty for full history (all events Censys still retains).\n\n\t e. **Censys Supplier Names** - Optional. Names of the suppliers to collect risk events for, separated by an asterisk (*). For example: OpenAI*Harvey*Acme, Inc. Names are not case-sensitive and can be picked from the Censys Supplier Risk UI. Names that are not found among your suppliers are skipped with a warning. Leave empty to collect all suppliers. A change applies from the next run and continues from the current position, so avoid changing it often. Maximum 500 characters.\n\n\t f. **Censys Risk Names** - Optional. Names of the risks to collect risk events for, separated by an asterisk (*). For example: Log4Shell*Exposed Redis. Names are not case-sensitive and must match the risk name shown in the Censys Supplier Risk UI. Names that are not found among the risks of your supply chain are skipped with a warning. Leave empty to collect all risks. A change applies from the next run and continues from the current position, so avoid changing it often. Maximum 500 characters.\n\n\t g. **Censys Change Types** - Optional. Comma-separated change types: opened, resolved, reopened. Other values stop the run with an error. Leave empty for all.\n\n\t h. **Censys Severity** - Optional. Comma-separated severities: CRITICAL, HIGH, MEDIUM, LOW. Other values stop the run with an error. Leave empty for all.\n\n\t i. **Censys Rapid Response** - Optional. Select true for rapid response findings only, false to exclude them. Leave empty for both.\n\n\t j. **Azure_Client_Id** - Enter the Azure client ID of the app registration you created.\n\n\t k. **Azure_Client_Secret** - Enter the client secret you created for the app registration.\n\n\t l. **Azure_Tenant_Id** - Enter the Azure tenant ID of your Microsoft Entra ID.\n\n\t m. **Azure_Entra_Object_Id** - Enter the object ID of your Microsoft Entra app (Enterprise application).\n\n\t n. **WorkspaceName** - Name of the Log Analytics workspace of Microsoft Sentinel. The deployment creates the table CensysRiskEvents_CL, a data collection endpoint and a data collection rule in it.\n\n\t o. **Location** - Region of the data collection endpoint and rule. It must be the region of the workspace. Default is the region of the resource group.\n\n\t p. **Censys Risk Events Table Name** - Name of the table in the workspace that the risk events are written to. Default is CensysRiskEvents_CL (the suffix _CL is added, or corrected, when it is missing or in another case). Maximum 52 characters. Warning: the parser, the workbook, the analytic rule and the connector status check of this solution read CensysRiskEvents_CL. If you use another name, update these resources to read the new table.\n\n\t q. **Censys Risk Events Collection Schedule** - Please enter a valid Quartz cron-expression, for example 0 */10 * * * *. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is 0 */10 * * * * (every 10 minutes).\n\n\t r. **Censys Supplier and Risk List Refresh Schedule** - Quartz cron-expression for how often the function that refreshes the lists of supplier names and risk names with their ids runs, for example 0 */30 * * * *. It also runs once when the app starts. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is every 30 minutes.\n\n\t s. **AppInsightsWorkspaceResourceID** - Use 'Log Analytic Workspace > Properties' blade having 'Resource ID' property value. This is a fully qualified resource ID which is in format '/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/Microsoft.OperationalInsights/workspaces/{workspace-name}'.\n\n\t t. **Censys SR Function Log Level** - Select log level or log severity value from DEBUG, INFO, ERROR. By default it is set to INFO.\n\n4. Mark the checkbox labeled **I agree to the terms and conditions stated above**. \n5. Click **Purchase** to deploy.", + "title": "Option 1 - Azure Resource Manager (ARM) Template" + }, + { + "description": "Use the following step-by-step instructions to deploy the Censys SR data connector manually with Azure Functions (Deployment via Visual Studio Code).", + "title": "Option 2 - Manual Deployment of Azure Functions" + }, + { + "description": "**1. Deploy a Function App**\n\n> **NOTE:** You will need to [prepare VS code](https://docs.microsoft.com/azure/azure-functions/functions-create-first-function-python#prerequisites) for Azure function development.\n\n1. Download the [Azure Function App](https://aka.ms/sentinel-CensysSR100-functionapp) file. Extract archive to your local development computer.\n2. Start VS Code. Choose File in the main menu and select Open Folder.\n3. Select the top level folder from extracted files.\n4. Choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose the **Deploy to function app** button.\nIf you aren't already signed in, choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose **Sign in to Azure**\nIf you're already signed in, go to the next step.\n5. Provide the following information at the prompts:\n\n\ta. **Select folder:** Choose a folder from your workspace or browse to one that contains your function app.\n\n\tb. **Select Subscription:** Choose the subscription to use.\n\n\tc. Select **Create new Function App in Azure** (Don't choose the Advanced option)\n\n\td. **Enter a globally unique name for the function app:** Type a name that is valid in a URL path. The name you type is validated to make sure that it's unique in Azure Functions. (e.g. CensysSRXXXXX).\n\n\te. **Select a runtime:** Choose Python 3.14 (the packages in the zip are built for it).\n\n\tf. Select a location for new resources. For better performance and lower costs choose the same [region](https://azure.microsoft.com/regions/) where Microsoft Sentinel is located.\n\n6. Deployment will begin. A notification is displayed after your function app is created and the deployment package is applied.\n7. Go to Azure Portal for the Function App configuration.\n8. In the Function App, enable the system-assigned managed identity if you want to use it for the function app storage.\n9. The manual method needs the table CensysRiskEvents_CL, a data collection endpoint and a data collection rule. Deploy the template of Option 1 once to create them, or create them yourself.\n10. Give the Microsoft Entra application the **Monitoring Metrics Publisher** role on the data collection rule: open the rule, select **Access control (IAM) > Add role assignment**, choose the role and select your application." + }, + { + "description": "**2. Configure the Function App**\n\n1. In the Function App, select the Function App Name and select **Configuration**.\n2. In the **Application settings** tab, select **+ New application setting**.\n3. Add each of the following application settings individually, with their respective values (case-sensitive): \n\n\t a. **CensysApiToken** - Enter your Censys personal access token.\n\n\t b. **OrganizationId** - Enter the UUID of the Censys organization, for example 00000000-0000-0000-0000-000000000000.\n\n\t c. **InitialStartTime** - Optional. UTC start time in the form 2026-10-01T00:00:00Z. It must not be in the future. Leave empty for all events Censys still retains.\n\n\t d. **SupplierNames** - Optional. Supplier names separated by an asterisk (*), for example OpenAI*Harvey*Acme, Inc. Pick the names from the Censys Supplier Risk UI. Maximum 500 characters.\n\n\t e. **RiskNames** - Optional. Risk names separated by an asterisk (*), for example Log4Shell*Exposed Redis. Pick the names from the Censys Supplier Risk UI. Maximum 500 characters.\n\n\t f. **ChangeTypes** - Optional. Comma-separated: opened, resolved, reopened. Other values stop the run with an error.\n\n\t g. **Severity** - Optional. Comma-separated: CRITICAL, HIGH, MEDIUM, LOW. Other values stop the run with an error.\n\n\t h. **RapidResponse** - Optional. true or false.\n\n\t i. **AZURE_CLIENT_ID** - Azure Client Id of your Microsoft Entra app.\n\n\t j. **AZURE_CLIENT_SECRET** - Client secret of your Microsoft Entra app.\n\n\t k. **AZURE_TENANT_ID** - Azure Tenant Id of your Microsoft Entra ID.\n\n\t l. **AZURE_DATA_COLLECTION_ENDPOINT** - Logs ingestion URL of the data collection endpoint created for this connector (Monitor > Data Collection Endpoints > the endpoint > Logs Ingestion).\n\n\t m. **AZURE_DATA_COLLECTION_RULE_ID** - Immutable ID of the data collection rule created for this connector (Monitor > Data Collection Rules > the rule > JSON View > immutableId).\n\n\t n. **StreamName** - Input stream of that data collection rule, Custom-CensysRiskEvents_CL.\n\n\t o. **SCOPE** - https://monitor.azure.com/.default\n\n\t p. **Schedule** - Quartz cron-expression, for example 0 */10 * * * *. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is 0 */10 * * * *.\n\n\t q. **SuppliersAndRisksSchedule** - Quartz cron-expression of the function that refreshes the supplier names and risk names with their ids. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is 0 */30 * * * *.\n\n\t r. **LogLevel** - DEBUG, INFO or ERROR. Default is INFO.\n\n4. Once all application settings have been entered, click **Save**." + } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId1'),'/'))))]", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId1'))]", + "contentId": "[variables('_dataConnectorContentId1')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorVersion1')]", + "source": { + "kind": "Solution", + "name": "Censys Supplier Risk", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Censys", + "email": "[variables('_email')]" + }, + "support": { + "name": "Censys Support", + "email": "support@censys.com", + "tier": "Partner", + "link": "https://docs.censys.com" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_dataConnectorContentId1')]", + "contentKind": "DataConnector", + "displayName": "Censys Supplier Risk (using Azure Functions)", + "contentProductId": "[variables('_dataConnectorcontentProductId1')]", + "id": "[variables('_dataConnectorcontentProductId1')]", + "version": "[variables('dataConnectorVersion1')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId1'),'/'))))]", + "dependsOn": [ + "[variables('_dataConnectorId1')]" + ], + "location": "[parameters('workspace-location')]", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId1'))]", + "contentId": "[variables('_dataConnectorContentId1')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorVersion1')]", + "source": { + "kind": "Solution", + "name": "Censys Supplier Risk", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Censys", + "email": "[variables('_email')]" + }, + "support": { + "name": "Censys Support", + "email": "support@censys.com", + "tier": "Partner", + "link": "https://docs.censys.com" + } + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId1'))]", + "apiVersion": "2021-03-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "GenericUI", + "properties": { + "connectorUiConfig": { + "title": "Censys Supplier Risk (using Azure Functions)", + "publisher": "Censys", + "descriptionMarkdown": "The Censys Supplier Risk data connector brings the Censys risk-events change feed (`/v1/supplychains/default/risk-events`) into Microsoft Sentinel using Azure Functions and the [Logs Ingestion API](https://learn.microsoft.com/azure/azure-monitor/logs/logs-ingestion-api-overview). The function runs on a schedule and remembers the `inserted_at` and `id` of the last event it ingested, so each run continues where the previous one stopped. Events are written to the table `CensysRiskEvents_CL` through a data collection endpoint and a data collection rule that the deployment template creates. Suppliers and risks can be selected by name; the connector keeps the supplier names and risk names with their ids in storage tables.", + "graphQueries": [ + { + "metricName": "Total Censys risk events received", + "legend": "CensysRiskEvents_CL", + "baseQuery": "CensysRiskEvents_CL" + } + ], + "dataTypes": [ + { + "name": "CensysRiskEvents_CL", + "lastDataReceivedQuery": "CensysRiskEvents_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + } + ], + "connectivityCriterias": [ + { + "type": "IsConnectedQuery", + "value": [ + "CensysRiskEvents_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + } + ], + "sampleQueries": [ + { + "description": "All Censys risk events received, newest first.", + "query": "CensysRiskEvents_CL\n| sort by TimeGenerated desc" + }, + { + "description": "Newly opened or reopened risks with critical or high severity.", + "query": "CensysRiskEvents_CL\n| where ChangeType in ('opened', 'reopened')\n| where Severity in ('CRITICAL', 'HIGH')\n| project TimeGenerated, Severity, RiskName, SupplierDisplayName, AssetIp, AssetHostname, PlatformUrl\n| order by TimeGenerated desc" + }, + { + "description": "Rapid response findings (actively exploited).", + "query": "CensysRiskEvents_CL\n| where RapidResponse == true and ChangeType in ('opened', 'reopened')\n| project TimeGenerated, Severity, CveId, RiskName, SupplierDisplayName, AssetIp, Evidence, PlatformUrl" + }, + { + "description": "Currently open risks per supplier (latest event per risk).", + "query": "CensysRiskEvents_CL\n| summarize arg_max(InsertedAt, *) by RiskInstanceId\n| where Status == 'open'\n| summarize OpenRisks = count() by SupplierDisplayName, Severity\n| order by OpenRisks desc" + }, + { + "description": "Risk events mapped to known CVEs.", + "query": "CensysRiskEvents_CL\n| where isnotempty(CveId)\n| summarize AffectedAssets = dcount(RiskInstanceId) by CveId, Severity\n| order by AffectedAssets desc" + } + ], + "availability": { + "status": 1, + "isPreview": false + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "read and write permissions on the workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ], + "customs": [ + { + "name": "Microsoft.Web/sites permissions", + "description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)." + }, + { + "name": "REST API Credentials/permissions", + "description": "A Censys personal access token and the Censys Organization ID are required. Users on Starter, Search, and Core plans need the API Access role. [Create a token](https://accounts.censys.io/settings/personal-access-tokens)." + }, + { + "name": "Microsoft Entra application", + "description": "An app registration with a client secret is required. Permission to create role assignments on the data collection rule (Owner or User Access Administrator) is required to give the application the Monitoring Metrics Publisher role." + } + ] + }, + "instructionSteps": [ + { + "description": ">**NOTE:** This connector uses Azure Functions to connect to the Censys API to pull risk events into Microsoft Sentinel using the [Logs Ingestion API](https://learn.microsoft.com/azure/azure-monitor/logs/logs-ingestion-api-overview). The deployment template creates the table CensysRiskEvents_CL, a data collection endpoint and a data collection rule in the workspace. A second function, CensysSRSuppliersAndRisks, keeps the lists of supplier names and risk names, with their ids, in storage tables of the Function App (nothing is sent to Microsoft Sentinel), so supplier names and risk names can be used as an input. This might result in additional data ingestion costs. Check the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/) for details." + }, + { + "description": ">**(Optional Step)** Securely store the Censys API token and the client secret in Azure Key Vault. Azure Key Vault provides a secure mechanism to store and retrieve key values. [Follow these instructions](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) to use Azure Key Vault with an Azure Function App." + }, + { + "description": "**STEP 1 - Steps to Create/Get a Censys Personal Access Token and Organization ID**\n\n Follow these instructions to get the values needed by this connector:\n 1. Sign in to Censys and open [Personal access tokens](https://accounts.censys.io/settings/personal-access-tokens).\n 2. Create a personal access token. Users on Starter, Search, and Core plans need the **API Access** role.\n 3. Copy the token. It is shown only once.\n 4. Copy the **Organization ID** (UUID) of the Censys organization you want to collect risk events for." + }, + { + "description": "**STEP 2 - Get the name of the Log Analytics workspace**\n\n The deployment template creates the table, the data collection endpoint and the data collection rule in the workspace of Microsoft Sentinel, so deploy it in the resource group of that workspace. You need the **name** of the workspace (workspace Overview page)." + }, + { + "description": "**STEP 3 - App Registration steps for the Application in Microsoft Entra ID**\n\n This integration requires an App registration in the Azure portal. Follow the steps in this section to create a new application in Microsoft Entra ID:\n 1. Sign in to the [Azure portal](https://portal.azure.com/).\n 2. Search for and select **Microsoft Entra ID**.\n 3. Under **Manage**, select **App registrations > New registration**.\n 4. Enter a display **Name** for your application.\n 5. Select **Register** to complete the initial app registration.\n 6. When registration finishes, the Azure portal displays the app registration's Overview pane. You see the **Application (client) ID** and **Tenant ID**. The client ID and Tenant ID are required as configuration parameters for the execution of the Censys SR data connector. \n\n> **Reference link:** [https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app)" + }, + { + "description": "**STEP 4 - Add a client secret for application in Microsoft Entra ID**\n\n Sometimes called an application password, a client secret is a string value required for the execution of the Censys SR data connector. Follow the steps in this section to create a new Client Secret:\n 1. In the Azure portal, in **App registrations**, select your application.\n 2. Select **Certificates & secrets > Client secrets > New client secret**.\n 3. Add a description for your client secret.\n 4. Select an expiration for the secret or specify a custom lifetime. Limit is 24 months.\n 5. Select **Add**. \n 6. *Record the secret's value for use in your client application code. This secret value is never displayed again after you leave this page.* The secret value is required as configuration parameter for the execution of the Censys SR data connector. \n\n> **Reference link:** [https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app#add-a-client-secret](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app#add-a-client-secret)" + }, + { + "description": "**STEP 5 - Get Object ID of your application in Microsoft Entra ID**\n\n After creating your app registration, follow the steps in this section to get Object ID:\n 1. Go to **Microsoft Entra ID**.\n 2. Select **Enterprise applications** from the left menu.\n 3. Find your newly created application in the list (you can search by the name you provided).\n 4. Click on the application.\n 5. On the overview page, copy the **Object ID**. This is the **Azure_Entra_Object_Id** needed for your ARM template role assignment. The template gives this application the **Monitoring Metrics Publisher** role on the data collection rule, so no other role needs to be assigned." + }, + { + "description": "**STEP 6 - Choose ONE from the following two deployment options to deploy the connector and the associated Azure Function**\n\n>**IMPORTANT:** Before deploying the Censys SR data connector, have the Censys API token, the organization ID, the workspace name and the Azure credentials (client ID, client secret, tenant ID, object ID) ready." + }, + { + "description": "Use this method for automated deployment of the Censys SR connector.\n\n1. Click the **Deploy to Azure** button below. \n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-CensysSRRiskEventsFunction-azuredeploy)\n2. Select the preferred **Subscription** and **Resource Group**. Use the resource group of the Log Analytics workspace of Microsoft Sentinel. \n3. Enter the below information : \n\n\t a. **Censys SR Function Name** - Name prefix of the Azure Function App to be created, up to 10 characters. Default is CensysSR.\n\n\t b. **Censys Api Key** - Enter your Censys personal access token.\n\n\t c. **Censys Organization Id** - Enter the UUID of the Censys organization to collect risk events for, for example 00000000-0000-0000-0000-000000000000.\n\n\t d. **Censys Start Date Time** - Optional. UTC time to start collecting from, in the form 2026-10-01T00:00:00Z. It must not be in the future. Leave empty for full history (all events Censys still retains).\n\n\t e. **Censys Supplier Names** - Optional. Names of the suppliers to collect risk events for, separated by an asterisk (*). For example: OpenAI*Harvey*Acme, Inc. Names are not case-sensitive and can be picked from the Censys Supplier Risk UI. Names that are not found among your suppliers are skipped with a warning. Leave empty to collect all suppliers. A change applies from the next run and continues from the current position, so avoid changing it often. Maximum 500 characters.\n\n\t f. **Censys Risk Names** - Optional. Names of the risks to collect risk events for, separated by an asterisk (*). For example: Log4Shell*Exposed Redis. Names are not case-sensitive and must match the risk name shown in the Censys Supplier Risk UI. Names that are not found among the risks of your supply chain are skipped with a warning. Leave empty to collect all risks. A change applies from the next run and continues from the current position, so avoid changing it often. Maximum 500 characters.\n\n\t g. **Censys Change Types** - Optional. Comma-separated change types: opened, resolved, reopened. Other values stop the run with an error. Leave empty for all.\n\n\t h. **Censys Severity** - Optional. Comma-separated severities: CRITICAL, HIGH, MEDIUM, LOW. Other values stop the run with an error. Leave empty for all.\n\n\t i. **Censys Rapid Response** - Optional. Select true for rapid response findings only, false to exclude them. Leave empty for both.\n\n\t j. **Azure_Client_Id** - Enter the Azure client ID of the app registration you created.\n\n\t k. **Azure_Client_Secret** - Enter the client secret you created for the app registration.\n\n\t l. **Azure_Tenant_Id** - Enter the Azure tenant ID of your Microsoft Entra ID.\n\n\t m. **Azure_Entra_Object_Id** - Enter the object ID of your Microsoft Entra app (Enterprise application).\n\n\t n. **WorkspaceName** - Name of the Log Analytics workspace of Microsoft Sentinel. The deployment creates the table CensysRiskEvents_CL, a data collection endpoint and a data collection rule in it.\n\n\t o. **Location** - Region of the data collection endpoint and rule. It must be the region of the workspace. Default is the region of the resource group.\n\n\t p. **Censys Risk Events Table Name** - Name of the table in the workspace that the risk events are written to. Default is CensysRiskEvents_CL (the suffix _CL is added, or corrected, when it is missing or in another case). Maximum 52 characters. Warning: the parser, the workbook, the analytic rule and the connector status check of this solution read CensysRiskEvents_CL. If you use another name, update these resources to read the new table.\n\n\t q. **Censys Risk Events Collection Schedule** - Please enter a valid Quartz cron-expression, for example 0 */10 * * * *. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is 0 */10 * * * * (every 10 minutes).\n\n\t r. **Censys Supplier and Risk List Refresh Schedule** - Quartz cron-expression for how often the function that refreshes the lists of supplier names and risk names with their ids runs, for example 0 */30 * * * *. It also runs once when the app starts. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is every 30 minutes.\n\n\t s. **AppInsightsWorkspaceResourceID** - Use 'Log Analytic Workspace > Properties' blade having 'Resource ID' property value. This is a fully qualified resource ID which is in format '/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/Microsoft.OperationalInsights/workspaces/{workspace-name}'.\n\n\t t. **Censys SR Function Log Level** - Select log level or log severity value from DEBUG, INFO, ERROR. By default it is set to INFO.\n\n4. Mark the checkbox labeled **I agree to the terms and conditions stated above**. \n5. Click **Purchase** to deploy.", + "title": "Option 1 - Azure Resource Manager (ARM) Template" + }, + { + "description": "Use the following step-by-step instructions to deploy the Censys SR data connector manually with Azure Functions (Deployment via Visual Studio Code).", + "title": "Option 2 - Manual Deployment of Azure Functions" + }, + { + "description": "**1. Deploy a Function App**\n\n> **NOTE:** You will need to [prepare VS code](https://docs.microsoft.com/azure/azure-functions/functions-create-first-function-python#prerequisites) for Azure function development.\n\n1. Download the [Azure Function App](https://aka.ms/sentinel-CensysSR100-functionapp) file. Extract archive to your local development computer.\n2. Start VS Code. Choose File in the main menu and select Open Folder.\n3. Select the top level folder from extracted files.\n4. Choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose the **Deploy to function app** button.\nIf you aren't already signed in, choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose **Sign in to Azure**\nIf you're already signed in, go to the next step.\n5. Provide the following information at the prompts:\n\n\ta. **Select folder:** Choose a folder from your workspace or browse to one that contains your function app.\n\n\tb. **Select Subscription:** Choose the subscription to use.\n\n\tc. Select **Create new Function App in Azure** (Don't choose the Advanced option)\n\n\td. **Enter a globally unique name for the function app:** Type a name that is valid in a URL path. The name you type is validated to make sure that it's unique in Azure Functions. (e.g. CensysSRXXXXX).\n\n\te. **Select a runtime:** Choose Python 3.14 (the packages in the zip are built for it).\n\n\tf. Select a location for new resources. For better performance and lower costs choose the same [region](https://azure.microsoft.com/regions/) where Microsoft Sentinel is located.\n\n6. Deployment will begin. A notification is displayed after your function app is created and the deployment package is applied.\n7. Go to Azure Portal for the Function App configuration.\n8. In the Function App, enable the system-assigned managed identity if you want to use it for the function app storage.\n9. The manual method needs the table CensysRiskEvents_CL, a data collection endpoint and a data collection rule. Deploy the template of Option 1 once to create them, or create them yourself.\n10. Give the Microsoft Entra application the **Monitoring Metrics Publisher** role on the data collection rule: open the rule, select **Access control (IAM) > Add role assignment**, choose the role and select your application." + }, + { + "description": "**2. Configure the Function App**\n\n1. In the Function App, select the Function App Name and select **Configuration**.\n2. In the **Application settings** tab, select **+ New application setting**.\n3. Add each of the following application settings individually, with their respective values (case-sensitive): \n\n\t a. **CensysApiToken** - Enter your Censys personal access token.\n\n\t b. **OrganizationId** - Enter the UUID of the Censys organization, for example 00000000-0000-0000-0000-000000000000.\n\n\t c. **InitialStartTime** - Optional. UTC start time in the form 2026-10-01T00:00:00Z. It must not be in the future. Leave empty for all events Censys still retains.\n\n\t d. **SupplierNames** - Optional. Supplier names separated by an asterisk (*), for example OpenAI*Harvey*Acme, Inc. Pick the names from the Censys Supplier Risk UI. Maximum 500 characters.\n\n\t e. **RiskNames** - Optional. Risk names separated by an asterisk (*), for example Log4Shell*Exposed Redis. Pick the names from the Censys Supplier Risk UI. Maximum 500 characters.\n\n\t f. **ChangeTypes** - Optional. Comma-separated: opened, resolved, reopened. Other values stop the run with an error.\n\n\t g. **Severity** - Optional. Comma-separated: CRITICAL, HIGH, MEDIUM, LOW. Other values stop the run with an error.\n\n\t h. **RapidResponse** - Optional. true or false.\n\n\t i. **AZURE_CLIENT_ID** - Azure Client Id of your Microsoft Entra app.\n\n\t j. **AZURE_CLIENT_SECRET** - Client secret of your Microsoft Entra app.\n\n\t k. **AZURE_TENANT_ID** - Azure Tenant Id of your Microsoft Entra ID.\n\n\t l. **AZURE_DATA_COLLECTION_ENDPOINT** - Logs ingestion URL of the data collection endpoint created for this connector (Monitor > Data Collection Endpoints > the endpoint > Logs Ingestion).\n\n\t m. **AZURE_DATA_COLLECTION_RULE_ID** - Immutable ID of the data collection rule created for this connector (Monitor > Data Collection Rules > the rule > JSON View > immutableId).\n\n\t n. **StreamName** - Input stream of that data collection rule, Custom-CensysRiskEvents_CL.\n\n\t o. **SCOPE** - https://monitor.azure.com/.default\n\n\t p. **Schedule** - Quartz cron-expression, for example 0 */10 * * * *. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is 0 */10 * * * *.\n\n\t q. **SuppliersAndRisksSchedule** - Quartz cron-expression of the function that refreshes the supplier names and risk names with their ids. **Warning:** keep the frequency at least 10 minutes for a better experience. Default is 0 */30 * * * *.\n\n\t r. **LogLevel** - DEBUG, INFO or ERROR. Default is INFO.\n\n4. Once all application settings have been entered, click **Save**." + } + ], + "id": "[variables('_uiConfigId1')]" + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('parserObject1').parserTemplateSpecName1]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "CensysRiskEvents Data Parser with template version 3.0.0", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('parserObject1').parserVersion1]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[variables('parserObject1')._parserName1]", + "apiVersion": "2025-07-01", + "type": "Microsoft.OperationalInsights/workspaces/savedSearches", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Parser for CensysRiskEvents", + "category": "Microsoft Sentinel Parser", + "functionAlias": "CensysRiskEvents", + "query": "let CensysRiskEvents_view = view () {\n // The empty datatable keeps the parser working (returning no rows) when the table does not exist yet\n union isfuzzy=true\n (datatable(\n TimeGenerated:datetime,\n EventId:string,\n InsertedAt:datetime,\n OccurredAt:datetime,\n ChangeType:string,\n RiskInstanceId:string,\n SupplyChainId:string,\n AssetType:string,\n RiskTypeId:string,\n CveId:string,\n RiskCategory:string,\n RiskName:string,\n Description:string,\n Remediations:dynamic,\n Severity:string,\n Score:real,\n Confidence:real,\n Status:string,\n ResolveTime:datetime,\n ResolveReason:string,\n SupplierId:string,\n SupplierDisplayName:string,\n Suppliers:dynamic,\n AssetIp:string,\n AssetHostname:string,\n AssetPort:int,\n ServiceTransportProtocol:string,\n ServicePort:int,\n ServiceProtocol:string,\n Tags:dynamic,\n RapidResponse:bool,\n Evidence:string,\n FirstDetectedAt:datetime,\n LastDetectedAt:datetime,\n CreatedAt:datetime,\n PlatformUrl:string,\n IngestedAt:datetime\n )[]),\n (CensysRiskEvents_CL | extend IngestedAt = ingestion_time())\n // Treat every field as optional: a column that is missing from the table becomes empty instead of failing the query\n | extend\n TimeGenerated = column_ifexists('TimeGenerated', datetime(null)),\n EventId = column_ifexists('EventId', ''),\n InsertedAt = column_ifexists('InsertedAt', datetime(null)),\n OccurredAt = column_ifexists('OccurredAt', datetime(null)),\n ChangeType = column_ifexists('ChangeType', ''),\n RiskInstanceId = column_ifexists('RiskInstanceId', ''),\n SupplyChainId = column_ifexists('SupplyChainId', ''),\n AssetType = column_ifexists('AssetType', ''),\n RiskTypeId = column_ifexists('RiskTypeId', ''),\n CveId = column_ifexists('CveId', ''),\n RiskCategory = column_ifexists('RiskCategory', ''),\n RiskName = column_ifexists('RiskName', ''),\n Description = column_ifexists('Description', ''),\n Remediations = column_ifexists('Remediations', dynamic(null)),\n Severity = column_ifexists('Severity', ''),\n Score = column_ifexists('Score', real(null)),\n Confidence = column_ifexists('Confidence', real(null)),\n Status = column_ifexists('Status', ''),\n ResolveTime = column_ifexists('ResolveTime', datetime(null)),\n ResolveReason = column_ifexists('ResolveReason', ''),\n SupplierId = column_ifexists('SupplierId', ''),\n SupplierDisplayName = column_ifexists('SupplierDisplayName', ''),\n Suppliers = column_ifexists('Suppliers', dynamic(null)),\n AssetIp = column_ifexists('AssetIp', ''),\n AssetHostname = column_ifexists('AssetHostname', ''),\n AssetPort = column_ifexists('AssetPort', int(null)),\n ServiceTransportProtocol = column_ifexists('ServiceTransportProtocol', ''),\n ServicePort = column_ifexists('ServicePort', int(null)),\n ServiceProtocol = column_ifexists('ServiceProtocol', ''),\n Tags = column_ifexists('Tags', dynamic(null)),\n RapidResponse = column_ifexists('RapidResponse', bool(null)),\n Evidence = column_ifexists('Evidence', ''),\n FirstDetectedAt = column_ifexists('FirstDetectedAt', datetime(null)),\n LastDetectedAt = column_ifexists('LastDetectedAt', datetime(null)),\n CreatedAt = column_ifexists('CreatedAt', datetime(null)),\n PlatformUrl = column_ifexists('PlatformUrl', '')\n // Delivery is at-least-once, so keep one row per event: the first copy that was ingested\n | summarize arg_min(IngestedAt, *) by EventId\n | extend\n EventVendor = \"Censys\",\n EventProduct = \"Censys Supplier Risk\",\n ChangeType = tolower(ChangeType),\n Status = tolower(Status),\n Severity = toupper(Severity),\n SupplierId = coalesce(tostring(SupplierId), tostring(Suppliers[0].id)),\n SupplierDisplayName = coalesce(tostring(SupplierDisplayName), tostring(Suppliers[0].display_name)),\n AssetName = iff(isnotempty(AssetHostname), AssetHostname, AssetIp),\n IsOpen = tolower(Status) == \"open\",\n SeverityRank = case(\n toupper(Severity) == \"CRITICAL\", 4,\n toupper(Severity) == \"HIGH\", 3,\n toupper(Severity) == \"MEDIUM\", 2,\n toupper(Severity) == \"LOW\", 1,\n 0)\n | project\n TimeGenerated,\n IngestedAt,\n EventId,\n InsertedAt,\n OccurredAt,\n ChangeType,\n RiskInstanceId,\n SupplyChainId,\n RiskTypeId,\n CveId,\n RiskCategory,\n RiskName,\n Description,\n Remediations,\n Severity,\n SeverityRank,\n Score,\n Confidence,\n Status,\n IsOpen,\n ResolveTime,\n ResolveReason,\n SupplierId,\n SupplierDisplayName,\n Suppliers,\n AssetType,\n AssetName,\n AssetIp,\n AssetHostname,\n AssetPort,\n ServiceTransportProtocol,\n ServicePort,\n ServiceProtocol,\n Tags,\n RapidResponse,\n Evidence,\n FirstDetectedAt,\n LastDetectedAt,\n CreatedAt,\n PlatformUrl,\n EventVendor,\n EventProduct\n};\nCensysRiskEvents_view\n", + "functionParameters": "", + "version": 2, + "tags": [ + { + "name": "description", + "value": "" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('parserObject1')._parserId1,'/'))))]", + "dependsOn": [ + "[variables('parserObject1')._parserId1]" + ], + "properties": { + "parentId": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'CensysRiskEvents')]", + "contentId": "[variables('parserObject1').parserContentId1]", + "kind": "Parser", + "version": "[variables('parserObject1').parserVersion1]", + "source": { + "name": "Censys Supplier Risk", + "kind": "Solution", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Censys", + "email": "[variables('_email')]" + }, + "support": { + "name": "Censys Support", + "email": "support@censys.com", + "tier": "Partner", + "link": "https://docs.censys.com" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('parserObject1').parserContentId1]", + "contentKind": "Parser", + "displayName": "Parser for CensysRiskEvents", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','pr','-', uniqueString(concat(variables('_solutionId'),'-','Parser','-',variables('parserObject1').parserContentId1,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','pr','-', uniqueString(concat(variables('_solutionId'),'-','Parser','-',variables('parserObject1').parserContentId1,'-', '1.0.0')))]", + "version": "[variables('parserObject1').parserVersion1]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/savedSearches", + "apiVersion": "2025-07-01", + "name": "[variables('parserObject1')._parserName1]", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Parser for CensysRiskEvents", + "category": "Microsoft Sentinel Parser", + "functionAlias": "CensysRiskEvents", + "query": "let CensysRiskEvents_view = view () {\n // The empty datatable keeps the parser working (returning no rows) when the table does not exist yet\n union isfuzzy=true\n (datatable(\n TimeGenerated:datetime,\n EventId:string,\n InsertedAt:datetime,\n OccurredAt:datetime,\n ChangeType:string,\n RiskInstanceId:string,\n SupplyChainId:string,\n AssetType:string,\n RiskTypeId:string,\n CveId:string,\n RiskCategory:string,\n RiskName:string,\n Description:string,\n Remediations:dynamic,\n Severity:string,\n Score:real,\n Confidence:real,\n Status:string,\n ResolveTime:datetime,\n ResolveReason:string,\n SupplierId:string,\n SupplierDisplayName:string,\n Suppliers:dynamic,\n AssetIp:string,\n AssetHostname:string,\n AssetPort:int,\n ServiceTransportProtocol:string,\n ServicePort:int,\n ServiceProtocol:string,\n Tags:dynamic,\n RapidResponse:bool,\n Evidence:string,\n FirstDetectedAt:datetime,\n LastDetectedAt:datetime,\n CreatedAt:datetime,\n PlatformUrl:string,\n IngestedAt:datetime\n )[]),\n (CensysRiskEvents_CL | extend IngestedAt = ingestion_time())\n // Treat every field as optional: a column that is missing from the table becomes empty instead of failing the query\n | extend\n TimeGenerated = column_ifexists('TimeGenerated', datetime(null)),\n EventId = column_ifexists('EventId', ''),\n InsertedAt = column_ifexists('InsertedAt', datetime(null)),\n OccurredAt = column_ifexists('OccurredAt', datetime(null)),\n ChangeType = column_ifexists('ChangeType', ''),\n RiskInstanceId = column_ifexists('RiskInstanceId', ''),\n SupplyChainId = column_ifexists('SupplyChainId', ''),\n AssetType = column_ifexists('AssetType', ''),\n RiskTypeId = column_ifexists('RiskTypeId', ''),\n CveId = column_ifexists('CveId', ''),\n RiskCategory = column_ifexists('RiskCategory', ''),\n RiskName = column_ifexists('RiskName', ''),\n Description = column_ifexists('Description', ''),\n Remediations = column_ifexists('Remediations', dynamic(null)),\n Severity = column_ifexists('Severity', ''),\n Score = column_ifexists('Score', real(null)),\n Confidence = column_ifexists('Confidence', real(null)),\n Status = column_ifexists('Status', ''),\n ResolveTime = column_ifexists('ResolveTime', datetime(null)),\n ResolveReason = column_ifexists('ResolveReason', ''),\n SupplierId = column_ifexists('SupplierId', ''),\n SupplierDisplayName = column_ifexists('SupplierDisplayName', ''),\n Suppliers = column_ifexists('Suppliers', dynamic(null)),\n AssetIp = column_ifexists('AssetIp', ''),\n AssetHostname = column_ifexists('AssetHostname', ''),\n AssetPort = column_ifexists('AssetPort', int(null)),\n ServiceTransportProtocol = column_ifexists('ServiceTransportProtocol', ''),\n ServicePort = column_ifexists('ServicePort', int(null)),\n ServiceProtocol = column_ifexists('ServiceProtocol', ''),\n Tags = column_ifexists('Tags', dynamic(null)),\n RapidResponse = column_ifexists('RapidResponse', bool(null)),\n Evidence = column_ifexists('Evidence', ''),\n FirstDetectedAt = column_ifexists('FirstDetectedAt', datetime(null)),\n LastDetectedAt = column_ifexists('LastDetectedAt', datetime(null)),\n CreatedAt = column_ifexists('CreatedAt', datetime(null)),\n PlatformUrl = column_ifexists('PlatformUrl', '')\n // Delivery is at-least-once, so keep one row per event: the first copy that was ingested\n | summarize arg_min(IngestedAt, *) by EventId\n | extend\n EventVendor = \"Censys\",\n EventProduct = \"Censys Supplier Risk\",\n ChangeType = tolower(ChangeType),\n Status = tolower(Status),\n Severity = toupper(Severity),\n SupplierId = coalesce(tostring(SupplierId), tostring(Suppliers[0].id)),\n SupplierDisplayName = coalesce(tostring(SupplierDisplayName), tostring(Suppliers[0].display_name)),\n AssetName = iff(isnotempty(AssetHostname), AssetHostname, AssetIp),\n IsOpen = tolower(Status) == \"open\",\n SeverityRank = case(\n toupper(Severity) == \"CRITICAL\", 4,\n toupper(Severity) == \"HIGH\", 3,\n toupper(Severity) == \"MEDIUM\", 2,\n toupper(Severity) == \"LOW\", 1,\n 0)\n | project\n TimeGenerated,\n IngestedAt,\n EventId,\n InsertedAt,\n OccurredAt,\n ChangeType,\n RiskInstanceId,\n SupplyChainId,\n RiskTypeId,\n CveId,\n RiskCategory,\n RiskName,\n Description,\n Remediations,\n Severity,\n SeverityRank,\n Score,\n Confidence,\n Status,\n IsOpen,\n ResolveTime,\n ResolveReason,\n SupplierId,\n SupplierDisplayName,\n Suppliers,\n AssetType,\n AssetName,\n AssetIp,\n AssetHostname,\n AssetPort,\n ServiceTransportProtocol,\n ServicePort,\n ServiceProtocol,\n Tags,\n RapidResponse,\n Evidence,\n FirstDetectedAt,\n LastDetectedAt,\n CreatedAt,\n PlatformUrl,\n EventVendor,\n EventProduct\n};\nCensysRiskEvents_view\n", + "functionParameters": "", + "version": 2, + "tags": [ + { + "name": "description", + "value": "" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "location": "[parameters('workspace-location')]", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('parserObject1')._parserId1,'/'))))]", + "dependsOn": [ + "[variables('parserObject1')._parserId1]" + ], + "properties": { + "parentId": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'CensysRiskEvents')]", + "contentId": "[variables('parserObject1').parserContentId1]", + "kind": "Parser", + "version": "[variables('parserObject1').parserVersion1]", + "source": { + "kind": "Solution", + "name": "Censys Supplier Risk", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Censys", + "email": "[variables('_email')]" + }, + "support": { + "name": "Censys Support", + "email": "support@censys.com", + "tier": "Partner", + "link": "https://docs.censys.com" + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('parserObject2').parserTemplateSpecName2]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "CensysRisks Data Parser with template version 3.0.0", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('parserObject2').parserVersion2]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[variables('parserObject2')._parserName2]", + "apiVersion": "2025-07-01", + "type": "Microsoft.OperationalInsights/workspaces/savedSearches", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Parser for Censys risks", + "category": "Microsoft Sentinel Parser", + "functionAlias": "CensysRisks", + "query": "// Current state of every supplier risk: the latest event of each risk instance, whether the risk is open or closed\nlet CensysRisks_view = view () {\n CensysRiskEvents\n | summarize arg_max(InsertedAt, *) by RiskInstanceId\n};\nCensysRisks_view\n", + "functionParameters": "", + "version": 2, + "tags": [ + { + "name": "description", + "value": "" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('parserObject2')._parserId2,'/'))))]", + "dependsOn": [ + "[variables('parserObject2')._parserId2]" + ], + "properties": { + "parentId": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'CensysRisks')]", + "contentId": "[variables('parserObject2').parserContentId2]", + "kind": "Parser", + "version": "[variables('parserObject2').parserVersion2]", + "source": { + "name": "Censys Supplier Risk", + "kind": "Solution", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Censys", + "email": "[variables('_email')]" + }, + "support": { + "name": "Censys Support", + "email": "support@censys.com", + "tier": "Partner", + "link": "https://docs.censys.com" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('parserObject2').parserContentId2]", + "contentKind": "Parser", + "displayName": "Parser for Censys risks", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','pr','-', uniqueString(concat(variables('_solutionId'),'-','Parser','-',variables('parserObject2').parserContentId2,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','pr','-', uniqueString(concat(variables('_solutionId'),'-','Parser','-',variables('parserObject2').parserContentId2,'-', '1.0.0')))]", + "version": "[variables('parserObject2').parserVersion2]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/savedSearches", + "apiVersion": "2025-07-01", + "name": "[variables('parserObject2')._parserName2]", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Parser for Censys risks", + "category": "Microsoft Sentinel Parser", + "functionAlias": "CensysRisks", + "query": "// Current state of every supplier risk: the latest event of each risk instance, whether the risk is open or closed\nlet CensysRisks_view = view () {\n CensysRiskEvents\n | summarize arg_max(InsertedAt, *) by RiskInstanceId\n};\nCensysRisks_view\n", + "functionParameters": "", + "version": 2, + "tags": [ + { + "name": "description", + "value": "" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "location": "[parameters('workspace-location')]", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('parserObject2')._parserId2,'/'))))]", + "dependsOn": [ + "[variables('parserObject2')._parserId2]" + ], + "properties": { + "parentId": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'CensysRisks')]", + "contentId": "[variables('parserObject2').parserContentId2]", + "kind": "Parser", + "version": "[variables('parserObject2').parserVersion2]", + "source": { + "kind": "Solution", + "name": "Censys Supplier Risk", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Censys", + "email": "[variables('_email')]" + }, + "support": { + "name": "Censys Support", + "email": "support@censys.com", + "tier": "Partner", + "link": "https://docs.censys.com" + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('workbookTemplateSpecName1')]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "CensysSupplierRisk Workbook with template version 3.0.0", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('workbookVersion1')]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Insights/workbooks", + "name": "[variables('workbookContentId1')]", + "location": "[parameters('workspace-location')]", + "kind": "shared", + "apiVersion": "2021-08-01", + "metadata": { + "description": "Gain insight into supplier risk collected from Censys Supplier Risk: open risks by severity, risk events by category, opened and closed risks over time, top risks by open risk count, supplier risk ranking and rapid response events, with details one selection away." + }, + "properties": { + "displayName": "[parameters('workbook1-name')]", + "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":1,\"content\":{\"json\":\"## Censys Supplier Risk Overview\\nOverview dashboard for Censys Supplier Risk risk events. All panels count risk events in the selected **Time Range**, one per event id, and every filter applies to every panel. Open counts use the status that each event carries, so a risk opened earlier than the Time Range with no event inside it will not appear.\"},\"name\":\"0a3c227f-21b3-55b9-b3d4-6c6d84922765\"},{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"3980b3ca-3e47-5259-ae54-e9cf79f7aa97\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"TimeRange\",\"label\":\"Time Range\",\"type\":4,\"isRequired\":true,\"value\":{\"durationMs\":604800000},\"typeSettings\":{\"selectableValues\":[{\"durationMs\":86400000},{\"durationMs\":259200000},{\"durationMs\":604800000},{\"durationMs\":1209600000},{\"durationMs\":2592000000}],\"allowCustom\":true}},{\"id\":\"531ff72e-136a-5683-8495-98755dec1185\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"Severity\",\"label\":\"Severity\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"timeContext\":{\"durationMs\":7776000000},\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"selectAllValue\":\"*\",\"showDefault\":false},\"defaultValue\":\"value::all\",\"value\":[\"value::all\"],\"jsonData\":\"[{\\\"value\\\": \\\"LOW\\\", \\\"label\\\": \\\"Low\\\"}, {\\\"value\\\": \\\"MEDIUM\\\", \\\"label\\\": \\\"Medium\\\"}, {\\\"value\\\": \\\"HIGH\\\", \\\"label\\\": \\\"High\\\"}, {\\\"value\\\": \\\"CRITICAL\\\", \\\"label\\\": \\\"Critical\\\"}]\"},{\"id\":\"408ed010-0624-5802-be49-0b16ce3b0b55\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"Supplier\",\"label\":\"Supplier\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"CensysRiskEvents\\n| where isnotempty(SupplierDisplayName)\\n| summarize by SupplierDisplayName\\n| project value = SupplierDisplayName, label = SupplierDisplayName\\n| order by label asc\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"timeContext\":{\"durationMs\":7776000000},\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"selectAllValue\":\"*\",\"showDefault\":false},\"defaultValue\":\"value::all\",\"value\":[\"value::all\"]},{\"id\":\"981f1274-a745-5685-9c0a-938042ba7b06\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"RiskCategory\",\"label\":\"Risk Category\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"timeContext\":{\"durationMs\":7776000000},\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"selectAllValue\":\"*\",\"showDefault\":false},\"defaultValue\":\"value::all\",\"value\":[\"value::all\"],\"jsonData\":\"[{\\\"value\\\": \\\"VULNERABILITY\\\", \\\"label\\\": \\\"Vulnerability\\\"}, {\\\"value\\\": \\\"MISCONFIGURATION\\\", \\\"label\\\": \\\"Misconfiguration\\\"}, {\\\"value\\\": \\\"EXPOSURE\\\", \\\"label\\\": \\\"Exposure\\\"}, {\\\"value\\\": \\\"THREAT\\\", \\\"label\\\": \\\"Threat\\\"}]\"},{\"id\":\"2a6c8d3e-7b1f-5f0e-9a44-3c2d9e8b1a77\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"Status\",\"label\":\"Status\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"timeContext\":{\"durationMs\":7776000000},\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"selectAllValue\":\"*\",\"showDefault\":false},\"defaultValue\":\"value::all\",\"value\":[\"value::all\"],\"jsonData\":\"[{\\\"value\\\": \\\"open\\\", \\\"label\\\": \\\"Open\\\"}, {\\\"value\\\": \\\"closed\\\", \\\"label\\\": \\\"Closed\\\"}]\"}],\"style\":\"pills\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\"},\"name\":\"d242da61-ec90-533c-946e-20debc8b18b2\"},{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"b0aac801-3580-5807-8df7-7a5e49f3c503\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"P1Severity\",\"type\":1,\"isRequired\":false,\"isHiddenWhenLocked\":true,\"value\":\"None\"},{\"id\":\"9952c2a7-5e8b-5f2f-a19d-dc9d340ca4fe\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"P2Date\",\"type\":1,\"isRequired\":false,\"isHiddenWhenLocked\":true,\"value\":\"None\"},{\"id\":\"c97c3004-6c39-55d2-8b8a-a3c41ef751f5\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"P2Status\",\"type\":1,\"isRequired\":false,\"isHiddenWhenLocked\":true,\"value\":\"None\"},{\"id\":\"c070c984-7e2c-568c-8d8f-b6ca8ffaffd8\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"P3Category\",\"type\":1,\"isRequired\":false,\"isHiddenWhenLocked\":true,\"value\":\"None\"},{\"id\":\"b5d774c8-0c6d-5ffa-8e74-193a9f8dcd6e\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"P4Risk\",\"type\":1,\"isRequired\":false,\"isHiddenWhenLocked\":true,\"value\":\"None\"},{\"id\":\"6179b664-6feb-5562-9189-6351e12c6713\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"P5Supplier\",\"type\":1,\"isRequired\":false,\"isHiddenWhenLocked\":true,\"value\":\"None\"},{\"id\":\"c728699c-76e3-5dd5-b847-ed58b40037b7\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"P6Event\",\"type\":1,\"isRequired\":false,\"isHiddenWhenLocked\":true,\"value\":\"None\"}],\"style\":\"pills\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\"},\"name\":\"383c127f-e50f-5a35-8def-1a9f9987899b\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"CensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| where Status == \\\"open\\\"\\n| summarize [\\\"count\\\"] = count() by severity = Severity\\n| order by severity asc\",\"size\":3,\"title\":\"Open Risk Count by Severity\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"piechart\",\"exportFieldName\":\"series\",\"exportParameterName\":\"P1Severity\",\"exportDefaultValue\":\"None\",\"chartSettings\":{\"group\":\"severity\",\"seriesLabelSettings\":[{\"seriesName\":\"CRITICAL\",\"color\":\"redBright\"},{\"seriesName\":\"HIGH\",\"color\":\"orange\"},{\"seriesName\":\"MEDIUM\",\"color\":\"yellow\"},{\"seriesName\":\"LOW\",\"color\":\"blue\"},{\"seriesName\":\"UNSPECIFIED\",\"color\":\"gray\"}],\"ySettings\":{\"numberFormatSettings\":{\"unit\":0,\"options\":{\"style\":\"decimal\",\"useGrouping\":true,\"maximumSignificantDigits\":15}}}}},\"name\":\"1b6bdd62-e666-568e-ad83-e6466e7d0772\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"CensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| summarize [\\\"count\\\"] = count() by category = RiskCategory\\n| order by category asc\",\"size\":3,\"title\":\"Risk Events by Category\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"piechart\",\"chartSettings\":{\"group\":\"category\",\"ySettings\":{\"numberFormatSettings\":{\"unit\":0,\"options\":{\"style\":\"decimal\",\"useGrouping\":true,\"maximumSignificantDigits\":15}}}},\"exportFieldName\":\"series\",\"exportParameterName\":\"P3Category\",\"exportDefaultValue\":\"None\"},\"name\":\"ab26b647-f261-532b-937b-fc06058cd3f2\",\"customWidth\":\"50\"},{\"type\":1,\"content\":{\"json\":\"💡 _Click on a slice in the above 'Open Risk Count by Severity' chart to view more details_\\n\\n📝 _Counts open risk events by severity, one per event id._\"},\"name\":\"3408c688-c583-5070-b687-a6b6c057b141\",\"customWidth\":\"50\"},{\"type\":1,\"content\":{\"json\":\"💡 _Click on a slice in the above 'Risk Events by Category' chart to view more details_\\n\\n📝 _Counts risk events by category, one per event id._\"},\"name\":\"a91116af-f6ff-5304-8e46-14afb20ca027\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"CensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| where Status == \\\"open\\\"\\n| where Severity == '{P1Severity}'\\n| order by InsertedAt desc\\n| project Time = InsertedAt, id = EventId, [\\\"Risk Name\\\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\\\"Platform URL\\\"] = PlatformUrl\\n| take 10000\",\"size\":0,\"title\":\"Events - {P1Severity}\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Time\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"id\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"20%\"}},{\"columnMatch\":\"Risk Name\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"27%\"}},{\"columnMatch\":\"Severity\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"8%\"}},{\"columnMatch\":\"Supplier\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"Platform URL\",\"formatter\":7,\"formatOptions\":{\"linkTarget\":\"Url\",\"linkLabel\":\"Open in Censys\",\"customColumnWidthSetting\":\"15%\"}}],\"filter\":true,\"rowLimit\":10000}},\"name\":\"b4afc41f-40bd-50d8-a9f6-66f8d2b30133\",\"customWidth\":\"100\",\"conditionalVisibility\":{\"parameterName\":\"P1Severity\",\"comparison\":\"isNotEqualTo\",\"value\":\"None\"}},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"CensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| where RiskCategory == '{P3Category}'\\n| order by InsertedAt desc\\n| project Time = InsertedAt, id = EventId, [\\\"Risk Name\\\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\\\"Platform URL\\\"] = PlatformUrl\\n| take 10000\",\"size\":0,\"title\":\"Events - {P3Category}\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Time\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"id\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"20%\"}},{\"columnMatch\":\"Risk Name\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"27%\"}},{\"columnMatch\":\"Severity\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"8%\"}},{\"columnMatch\":\"Supplier\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"Platform URL\",\"formatter\":7,\"formatOptions\":{\"linkTarget\":\"Url\",\"linkLabel\":\"Open in Censys\",\"customColumnWidthSetting\":\"15%\"}}],\"filter\":true,\"rowLimit\":10000}},\"name\":\"70e7cbe8-0bc7-571e-81e4-7d792f321c2b\",\"customWidth\":\"100\",\"conditionalVisibility\":{\"parameterName\":\"P3Category\",\"comparison\":\"isNotEqualTo\",\"value\":\"None\"}},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let TotalHours = (todatetime({TimeRange:end}) - todatetime({TimeRange:start})) / 1h;\\n// Time span picked from the Time Range (at most about 100 points): 30 minutes, 1 hour or 1 day\\nlet Span = iff(TotalHours <= 50, 30m, iff(TotalHours <= 100, 1h, 1d));\\nlet Buckets = range Bucket from bin(todatetime({TimeRange:start}), Span) to todatetime({TimeRange:end}) step Span | extend k = 1;\\nlet Statuses = datatable(Series: string)[\\\"Opened\\\", \\\"Closed\\\"] | extend k = 1;\\nlet Counts = CensysRiskEvents\\n | where OccurredAt {TimeRange}\\n | where ('*' in ({Status}) or Status in ({Status}))\\n | where ('*' in ({Severity}) or Severity in ({Severity}))\\n | where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n | where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n | extend Series = case(Status == \\\"open\\\", \\\"Opened\\\", Status == \\\"closed\\\", \\\"Closed\\\", Status)\\n | summarize Total = count() by Bucket = bin(OccurredAt, Span), Series;\\nBuckets\\n| join kind=inner Statuses on k\\n| join kind=leftouter Counts on Bucket, Series\\n| project Time = iff(Span >= 1d, format_datetime(Bucket, 'yyyy-MM-dd'), format_datetime(Bucket, 'yyyy-MM-dd HH:mm')), Series, [\\\"Risk Count\\\"] = coalesce(Total, 0)\\n| order by Time asc, Series asc\",\"size\":0,\"title\":\"Opened vs. Closed Risks\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"timechart\",\"chartSettings\":{\"xAxis\":\"Time\",\"group\":\"Series\",\"xSettings\":{\"label\":\"Time\"},\"ySettings\":{\"label\":\"Risk Count\"}},\"exportedParameters\":[{\"fieldName\":\"x\",\"parameterName\":\"P2Date\",\"parameterType\":1,\"defaultValue\":\"None\"},{\"fieldName\":\"series\",\"parameterName\":\"P2Status\",\"parameterType\":1,\"defaultValue\":\"None\"}]},\"name\":\"4668c9cb-64a5-581d-b461-d8854d0f2409\",\"customWidth\":\"100\"},{\"type\":1,\"content\":{\"json\":\"💡 _Click on a data point in the above 'Opened vs. Closed Risks' chart to view more details_\\n\\n📝 _Counts risk events per day by their status (Opened or Closed), by when they occurred._\"},\"name\":\"c2beca44-a16d-5fa8-821b-d7aea664ec9f\",\"customWidth\":\"100\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let TotalHours = (todatetime({TimeRange:end}) - todatetime({TimeRange:start})) / 1h;\\n// Time span picked from the Time Range (at most about 100 points): 30 minutes, 1 hour or 1 day\\nlet Span = iff(TotalHours <= 50, 30m, iff(TotalHours <= 100, 1h, 1d));\\nCensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| where Status == iff('{P2Status}' == \\\"Opened\\\", \\\"open\\\", \\\"closed\\\")\\n| where OccurredAt >= todatetime(substring('{P2Date}', 0, 16)) and OccurredAt < todatetime(substring('{P2Date}', 0, 16)) + Span\\n| order by InsertedAt desc\\n| project Time = InsertedAt, id = EventId, [\\\"Risk Name\\\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\\\"Platform URL\\\"] = PlatformUrl\\n| take 10000\",\"size\":0,\"title\":\"Events - {P2Status} on {P2Date}\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Time\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"id\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"20%\"}},{\"columnMatch\":\"Risk Name\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"27%\"}},{\"columnMatch\":\"Severity\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"8%\"}},{\"columnMatch\":\"Supplier\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"Platform URL\",\"formatter\":7,\"formatOptions\":{\"linkTarget\":\"Url\",\"linkLabel\":\"Open in Censys\",\"customColumnWidthSetting\":\"15%\"}}],\"filter\":true,\"rowLimit\":10000}},\"name\":\"50857b22-679b-5891-bd85-96262bd29d80\",\"customWidth\":\"100\",\"conditionalVisibilities\":[{\"parameterName\":\"P2Date\",\"comparison\":\"isNotEqualTo\",\"value\":\"None\"},{\"parameterName\":\"P2Status\",\"comparison\":\"isNotEqualTo\",\"value\":\"None\"}]},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"CensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| where Status == \\\"open\\\" and isnotempty(RiskName)\\n| summarize OpenRisks = count() by [\\\"Risk Name\\\"] = RiskName\\n| order by OpenRisks desc\\n| take 10\",\"size\":0,\"title\":\"Top 10 Risks by Open Risk Count\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Risk Name\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"80%\"}},{\"columnMatch\":\"OpenRisks\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"20%\"}}],\"filter\":true,\"rowLimit\":10000},\"exportFieldName\":\"Risk Name\",\"exportParameterName\":\"P4Risk\",\"exportDefaultValue\":\"None\"},\"name\":\"4c44d1fa-a2d9-55b0-98e5-8030350c5f9d\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"CensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| where Status == \\\"open\\\"\\n| summarize OpenRisks = count() by Supplier = SupplierDisplayName\\n| order by OpenRisks desc\",\"size\":0,\"title\":\"Supplier Risk Ranking\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Supplier\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"80%\"}},{\"columnMatch\":\"OpenRisks\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"20%\"}}],\"filter\":true,\"rowLimit\":10000},\"exportFieldName\":\"Supplier\",\"exportParameterName\":\"P5Supplier\",\"exportDefaultValue\":\"None\"},\"name\":\"a5821846-0945-540c-9c97-849cad7afa8b\",\"customWidth\":\"50\"},{\"type\":1,\"content\":{\"json\":\"💡 _Click on a row in the above 'Top 10 Risks by Open Risk Count' grid to view more details_\\n\\n📝 _The 10 risk names with the most open risk events._\"},\"name\":\"cdf92177-e783-5d83-97d1-37d122085500\",\"customWidth\":\"50\"},{\"type\":1,\"content\":{\"json\":\"💡 _Click on a row in the above 'Supplier Risk Ranking' grid to view more details_\\n\\n📝 _Suppliers ranked by their number of open risk events._\"},\"name\":\"4e9590ae-6386-5f24-9ca0-f3a096994918\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"CensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| where Status == \\\"open\\\"\\n| where RiskName == '{P4Risk}'\\n| order by InsertedAt desc\\n| project Time = InsertedAt, id = EventId, [\\\"Risk Name\\\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\\\"Platform URL\\\"] = PlatformUrl\\n| take 10000\",\"size\":0,\"title\":\"Events - {P4Risk}\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Time\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"id\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"20%\"}},{\"columnMatch\":\"Risk Name\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"27%\"}},{\"columnMatch\":\"Severity\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"8%\"}},{\"columnMatch\":\"Supplier\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"Platform URL\",\"formatter\":7,\"formatOptions\":{\"linkTarget\":\"Url\",\"linkLabel\":\"Open in Censys\",\"customColumnWidthSetting\":\"15%\"}}],\"filter\":true,\"rowLimit\":10000}},\"name\":\"22004c2d-0bf3-5e3d-9870-78807252bd52\",\"customWidth\":\"100\",\"conditionalVisibility\":{\"parameterName\":\"P4Risk\",\"comparison\":\"isNotEqualTo\",\"value\":\"None\"}},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"CensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| where Status == \\\"open\\\"\\n| where SupplierDisplayName == '{P5Supplier}'\\n| order by InsertedAt desc\\n| project Time = InsertedAt, id = EventId, [\\\"Risk Name\\\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\\\"Platform URL\\\"] = PlatformUrl\\n| take 10000\",\"size\":0,\"title\":\"Events - {P5Supplier}\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Time\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"id\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"20%\"}},{\"columnMatch\":\"Risk Name\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"27%\"}},{\"columnMatch\":\"Severity\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"8%\"}},{\"columnMatch\":\"Supplier\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"Platform URL\",\"formatter\":7,\"formatOptions\":{\"linkTarget\":\"Url\",\"linkLabel\":\"Open in Censys\",\"customColumnWidthSetting\":\"15%\"}}],\"filter\":true,\"rowLimit\":10000}},\"name\":\"67930e57-64b3-52a9-b227-bebac1f40c88\",\"customWidth\":\"100\",\"conditionalVisibility\":{\"parameterName\":\"P5Supplier\",\"comparison\":\"isNotEqualTo\",\"value\":\"None\"}},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"CensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| where RapidResponse == true\\n| order by InsertedAt desc\\n| project Time = InsertedAt, id = EventId, [\\\"Risk Name\\\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\\\"Platform URL\\\"] = PlatformUrl\\n| take 10000\",\"size\":0,\"title\":\"Rapid Response Events\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Time\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"id\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"20%\"}},{\"columnMatch\":\"Risk Name\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"27%\"}},{\"columnMatch\":\"Severity\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"8%\"}},{\"columnMatch\":\"Supplier\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"15%\"}},{\"columnMatch\":\"Platform URL\",\"formatter\":7,\"formatOptions\":{\"linkTarget\":\"Url\",\"linkLabel\":\"Open in Censys\",\"customColumnWidthSetting\":\"15%\"}}],\"filter\":true,\"rowLimit\":10000},\"exportFieldName\":\"id\",\"exportParameterName\":\"P6Event\",\"exportDefaultValue\":\"None\"},\"name\":\"d29db818-909d-5958-8ef4-89e50ee80542\",\"customWidth\":\"100\"},{\"type\":1,\"content\":{\"json\":\"💡 _Click on a row in the above 'Rapid Response Events' grid to view more details_\\n\\n📝 _Risk events flagged for rapid response (actively exploited), newest first._\"},\"name\":\"59f191f6-06f8-5ad8-b72b-8394af1f9a2a\",\"customWidth\":\"100\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"CensysRiskEvents\\n| where OccurredAt {TimeRange}\\n| where ('*' in ({Status}) or Status in ({Status}))\\n| where ('*' in ({Severity}) or Severity in ({Severity}))\\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\\n| where EventId == '{P6Event}'\\n| project Time = InsertedAt, id = EventId, [\\\"Change Type\\\"] = ChangeType, Status, [\\\"Risk Name\\\"] = RiskName, Severity, Supplier = SupplierDisplayName, Asset = AssetName, [\\\"Service Port\\\"] = ServicePort, [\\\"Resolve Reason\\\"] = ResolveReason, [\\\"Platform URL\\\"] = PlatformUrl\\n| take 10000\",\"size\":0,\"title\":\"Event - {P6Event}\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"showRefreshButton\":true,\"showAnalytics\":true,\"showExportToExcel\":true,\"noDataMessage\":\"No data found for the selected filters.\",\"timeContext\":{\"durationMs\":7776000000},\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Time\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"12%\"}},{\"columnMatch\":\"id\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"16%\"}},{\"columnMatch\":\"Change Type\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"8%\"}},{\"columnMatch\":\"Status\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"7%\"}},{\"columnMatch\":\"Risk Name\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"20%\"}},{\"columnMatch\":\"Severity\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"7%\"}},{\"columnMatch\":\"Supplier\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"11%\"}},{\"columnMatch\":\"Asset\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"10%\"}},{\"columnMatch\":\"Service Port\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"4%\"}},{\"columnMatch\":\"Resolve Reason\",\"formatter\":0,\"formatOptions\":{\"customColumnWidthSetting\":\"5%\"}},{\"columnMatch\":\"Platform URL\",\"formatter\":7,\"formatOptions\":{\"linkTarget\":\"Url\",\"linkLabel\":\"Open in Censys\",\"customColumnWidthSetting\":\"10%\"}}],\"filter\":true,\"rowLimit\":10000}},\"name\":\"996c3d06-11cb-52d7-86a5-2a6444795041\",\"customWidth\":\"100\",\"conditionalVisibility\":{\"parameterName\":\"P6Event\",\"comparison\":\"isNotEqualTo\",\"value\":\"None\"}}],\"fromTemplateId\":\"sentinel-CensysSupplierRisk\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\n", + "version": "1.0", + "sourceId": "[variables('workspaceResourceId')]", + "category": "sentinel" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Workbook-', last(split(variables('workbookId1'),'/'))))]", + "properties": { + "description": "@{workbookKey=CensysSupplierRiskWorkbook; logoFileName=Censys.svg; description=Gain insight into supplier risk collected from Censys Supplier Risk: open risks by severity, risk events by category, opened and closed risks over time, top risks by open risk count, supplier risk ranking and rapid response events, with details one selection away.; dataTypesDependencies=System.Object[]; dataConnectorsDependencies=System.Object[]; previewImagesFileNames=System.Object[]; version=1.0.0; title=Censys Supplier Risk; templateRelativePath=CensysSupplierRisk.json; subtitle=; provider=Censys}.description", + "parentId": "[variables('workbookId1')]", + "contentId": "[variables('_workbookContentId1')]", + "kind": "Workbook", + "version": "[variables('workbookVersion1')]", + "source": { + "kind": "Solution", + "name": "Censys Supplier Risk", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Censys", + "email": "[variables('_email')]" + }, + "support": { + "name": "Censys Support", + "email": "support@censys.com", + "tier": "Partner", + "link": "https://docs.censys.com" + }, + "dependencies": { + "operator": "AND", + "criteria": [ + { + "contentId": "CensysRiskEvents_CL", + "kind": "DataType" + }, + { + "contentId": "CensysSRRiskEventsFunction", + "kind": "DataConnector" + } + ] + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_workbookContentId1')]", + "contentKind": "Workbook", + "displayName": "[parameters('workbook1-name')]", + "contentProductId": "[variables('_workbookcontentProductId1')]", + "id": "[variables('_workbookcontentProductId1')]", + "version": "[variables('workbookVersion1')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject1').analyticRuleTemplateSpecName1]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "CensysRiskStateChange_AnalyticalRules Analytics Rule with template version 3.0.0", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject1').analyticRuleVersion1]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject1')._analyticRulecontentId1]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Creates one incident per supplier risk instance that is opened, or reopened, with CRITICAL or HIGH severity. Alerts are grouped by RiskInstanceId, so further events of the same risk instance within 24 hours are added to the same incident while it is open.\nAlert severity follows the Censys severity: CRITICAL and HIGH map to High, MEDIUM to Medium and LOW to Low. Resolved events follow the same mapping, and any other severity is Informational.\nBy default the rule alerts only on new CRITICAL and HIGH risks. The query holds commented lines to follow the whole life of a risk instead (reopened events of any severity and resolved events), see the comments in the query.", + "displayName": "Censys SR - Risk State Change", + "enabled": false, + "query": "// New events are selected by the time they reached Sentinel (IngestedAt), not by the Censys insert time,\n// so a delay between Censys and the connector can never make an event fall between two runs.\n// Each run covers 60 minutes that end 5 minutes ago, and consecutive runs are contiguous, so no event is alerted twice.\n//\n// OPTIONAL - follow the whole life of a risk instance (reopened events of any severity, and resolved events of risks that\n// were alerted before). To use it, uncomment the two \"let\" lines below, comment out the line \"Default\" in the query, uncomment\n// the line \"Full lifecycle\", and in the rule settings set the query period to 7 days and the incident grouping look-back to 7 days.\n// let Alertable = materialize(CensysRiskEvents | where ChangeType == \"reopened\" or (ChangeType == \"opened\" and Severity in (\"CRITICAL\", \"HIGH\")));\n// let TrackedRisks = Alertable | distinct RiskInstanceId;\nCensysRiskEvents\n| where IngestedAt between (ago(65m) .. ago(5m))\n// Default: opened or reopened risks with CRITICAL or HIGH severity\n| where ChangeType in (\"opened\", \"reopened\") and Severity in (\"CRITICAL\", \"HIGH\")\n// Full lifecycle (needs the optional \"let\" lines above):\n// | where (ChangeType == \"reopened\" or (ChangeType == \"opened\" and Severity in (\"CRITICAL\", \"HIGH\"))) or (ChangeType == \"resolved\" and RiskInstanceId in (TrackedRisks))\n//\n// Optional filters: remove the leading // of a line to use it. Each line narrows the alerts further, and several lines can be combined.\n// Do not remove or change the IngestedAt window above, it keeps every event alerted exactly once.\n// | where Confidence >= 0.8 // only findings with a confidence of 80% or more\n// | where Severity == \"CRITICAL\" // only CRITICAL risks (Censys severities: CRITICAL, HIGH, MEDIUM, LOW)\n// | where ChangeType == \"opened\" // only newly opened risks, skip reopened risks\n// | where RapidResponse == true // only actively exploited (rapid response) risks\n// | where RiskCategory == \"VULNERABILITY\" // VULNERABILITY, MISCONFIGURATION, EXPOSURE or THREAT\n// | where SupplierDisplayName in (\"OpenAI\", \"Harvey\") // only these suppliers\n// | where CveId != \"\" // only risks that have a CVE id\n// | where RiskName has \"Log4Shell\" // only risks whose name contains this text\n// | where AssetType == \"host\" // asset type: host or web\n| extend SentinelSeverity = case(\n Severity in (\"CRITICAL\", \"HIGH\"), \"High\",\n Severity == \"MEDIUM\", \"Medium\",\n Severity == \"LOW\", \"Low\",\n \"Informational\")\n| project TimeGenerated, IngestedAt, EventId, InsertedAt, OccurredAt, ChangeType, RiskInstanceId, Severity, SentinelSeverity,\n RiskCategory, RiskName, Description, CveId, RapidResponse, SupplierDisplayName,\n AssetName, AssetHostname, AssetIp, ServicePort, ServiceProtocol, Evidence, ResolveTime, ResolveReason, PlatformUrl\n", + "queryFrequency": "PT1H", + "queryPeriod": "P1D", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "connectorId": "CensysSRRiskEventsFunction", + "dataTypes": [ + "CensysRiskEvents_CL" + ] + } + ], + "tactics": [ + "InitialAccess" + ], + "techniques": [ + "T1195" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "columnName": "AssetHostname", + "identifier": "HostName" + } + ] + }, + { + "entityType": "IP", + "fieldMappings": [ + { + "columnName": "AssetIp", + "identifier": "Address" + } + ] + }, + { + "entityType": "URL", + "fieldMappings": [ + { + "columnName": "PlatformUrl", + "identifier": "Url" + } + ] + } + ], + "eventGroupingSettings": { + "aggregationKind": "AlertPerResult" + }, + "customDetails": { + "CensysSeverity": "Severity", + "RapidResponse": "RapidResponse", + "RiskInstanceId": "[if(true(), 'RiskInstanceId', 'RiskInstanceId')]", + "Evidence": "Evidence", + "RiskCategory": "RiskCategory", + "Supplier": "SupplierDisplayName", + "ResolveTime": "ResolveTime", + "CveId": "[if(true(), 'CveId', 'CveId')]", + "ChangeType": "ChangeType", + "ResolveReason": "ResolveReason" + }, + "alertDetailsOverride": { + "alertDynamicProperties": [ + { + "alertProperty": "AlertLink", + "value": "PlatformUrl" + } + ], + "alertSeverityColumnName": "SentinelSeverity", + "alertDisplayNameFormat": "Censys: {{ChangeType}} risk - {{RiskName}} ({{SupplierDisplayName}})", + "alertDescriptionFormat": "{{Description}} Asset: {{AssetName}}. Open in Censys: {{PlatformUrl}}" + }, + "incidentConfiguration": { + "groupingConfiguration": { + "enabled": true, + "matchingMethod": "Selected", + "lookbackDuration": "P1D", + "groupByCustomDetails": [ + "RiskInstanceId" + ], + "reopenClosedIncident": false + }, + "createIncident": true + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject1').analyticRuleId1,'/'))))]", + "properties": { + "description": "Censys Supplier Risk Analytics Rule 1", + "parentId": "[variables('analyticRuleObject1').analyticRuleId1]", + "contentId": "[variables('analyticRuleObject1')._analyticRulecontentId1]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject1').analyticRuleVersion1]", + "source": { + "kind": "Solution", + "name": "Censys Supplier Risk", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Censys", + "email": "[variables('_email')]" + }, + "support": { + "name": "Censys Support", + "email": "support@censys.com", + "tier": "Partner", + "link": "https://docs.censys.com" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject1')._analyticRulecontentId1]", + "contentKind": "AnalyticsRule", + "displayName": "Censys SR - Risk State Change", + "contentProductId": "[variables('analyticRuleObject1')._analyticRulecontentProductId1]", + "id": "[variables('analyticRuleObject1')._analyticRulecontentProductId1]", + "version": "[variables('analyticRuleObject1').analyticRuleVersion1]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentPackages", + "apiVersion": "2023-04-01-preview", + "location": "[parameters('workspace-location')]", + "properties": { + "version": "3.0.0", + "kind": "Solution", + "contentSchemaVersion": "3.0.0", + "displayName": "Censys Supplier Risk", + "publisherDisplayName": "Censys Support", + "descriptionHtml": "

Note: Please refer to the following before installing the solution:

\n

• Review the solution Release Notes

\n

• There may be known issues pertaining to this Solution, please refer to them before installing.

\n

Bring Censys Supplier Risk into Microsoft Sentinel. The solution collects supply chain risk events (risks opened, reopened and resolved for your suppliers) from the Censys platform, and provides a parser, a workbook and an analytic rule that raises incidents for new and reopened supplier risks and keeps resolved events with the incident of the same risk.

\n

Data Connectors: 1, Parsers: 2, Workbooks: 1, Analytic Rules: 1

\n

Learn more about Microsoft Sentinel | Learn more about Solutions

\n", + "contentKind": "Solution", + "contentProductId": "[variables('_solutioncontentProductId')]", + "id": "[variables('_solutioncontentProductId')]", + "icon": "", + "contentId": "[variables('_solutionId')]", + "parentId": "[variables('_solutionId')]", + "source": { + "kind": "Solution", + "name": "Censys Supplier Risk", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Censys", + "email": "[variables('_email')]" + }, + "support": { + "name": "Censys Support", + "email": "support@censys.com", + "tier": "Partner", + "link": "https://docs.censys.com" + }, + "dependencies": { + "operator": "AND", + "criteria": [ + { + "kind": "DataConnector", + "contentId": "[variables('_dataConnectorContentId1')]", + "version": "[variables('dataConnectorVersion1')]" + }, + { + "kind": "Parser", + "contentId": "[variables('parserObject1').parserContentId1]", + "version": "[variables('parserObject1').parserVersion1]" + }, + { + "kind": "Parser", + "contentId": "[variables('parserObject2').parserContentId2]", + "version": "[variables('parserObject2').parserVersion2]" + }, + { + "kind": "Workbook", + "contentId": "[variables('_workbookContentId1')]", + "version": "[variables('workbookVersion1')]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject1')._analyticRulecontentId1]", + "version": "[variables('analyticRuleObject1').analyticRuleVersion1]" + } + ] + }, + "firstPublishDate": "2026-10-29", + "lastPublishDate": "2026-10-29", + "providers": [ + "Censys" + ], + "categories": { + "domains": [ + "Security - Network", + "Security - Cloud Security" + ] + } + }, + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('_solutionId'))]" + } + ], + "outputs": {} +} diff --git a/Solutions/Censys Supplier Risk/Package/testParameters.json b/Solutions/Censys Supplier Risk/Package/testParameters.json new file mode 100644 index 00000000000..855e4aba240 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Package/testParameters.json @@ -0,0 +1,32 @@ +{ + "location": { + "type": "string", + "minLength": 1, + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Not used, but needed to pass arm-ttk test `Location-Should-Not-Be-Hardcoded`. We instead use the `workspace-location` which is derived from the LA workspace" + } + }, + "workspace-location": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "[concat('Region to deploy solution resources -- separate from location selection',parameters('location'))]" + } + }, + "workspace": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" + } + }, + "workbook1-name": { + "type": "string", + "defaultValue": "Censys Supplier Risk", + "minLength": 1, + "metadata": { + "description": "Name for the workbook" + } + } +} diff --git a/Solutions/Censys Supplier Risk/Parsers/CensysRiskEvents.yaml b/Solutions/Censys Supplier Risk/Parsers/CensysRiskEvents.yaml new file mode 100644 index 00000000000..7bee4a5733a --- /dev/null +++ b/Solutions/Censys Supplier Risk/Parsers/CensysRiskEvents.yaml @@ -0,0 +1,153 @@ +id: 5d2a8c1e-7b34-4f6a-9e0d-3c81a2b4f917 +Function: + Title: Parser for CensysRiskEvents + Version: "1.0.0" + LastUpdated: "2026-10-15" +Category: Microsoft Sentinel Parser +FunctionName: CensysRiskEvents +FunctionAlias: CensysRiskEvents +FunctionQuery: | + let CensysRiskEvents_view = view () { + // The empty datatable keeps the parser working (returning no rows) when the table does not exist yet + union isfuzzy=true + (datatable( + TimeGenerated:datetime, + EventId:string, + InsertedAt:datetime, + OccurredAt:datetime, + ChangeType:string, + RiskInstanceId:string, + SupplyChainId:string, + AssetType:string, + RiskTypeId:string, + CveId:string, + RiskCategory:string, + RiskName:string, + Description:string, + Remediations:dynamic, + Severity:string, + Score:real, + Confidence:real, + Status:string, + ResolveTime:datetime, + ResolveReason:string, + SupplierId:string, + SupplierDisplayName:string, + Suppliers:dynamic, + AssetIp:string, + AssetHostname:string, + AssetPort:int, + ServiceTransportProtocol:string, + ServicePort:int, + ServiceProtocol:string, + Tags:dynamic, + RapidResponse:bool, + Evidence:string, + FirstDetectedAt:datetime, + LastDetectedAt:datetime, + CreatedAt:datetime, + PlatformUrl:string, + IngestedAt:datetime + )[]), + (CensysRiskEvents_CL | extend IngestedAt = ingestion_time()) + // Treat every field as optional: a column that is missing from the table becomes empty instead of failing the query + | extend + TimeGenerated = column_ifexists('TimeGenerated', datetime(null)), + EventId = column_ifexists('EventId', ''), + InsertedAt = column_ifexists('InsertedAt', datetime(null)), + OccurredAt = column_ifexists('OccurredAt', datetime(null)), + ChangeType = column_ifexists('ChangeType', ''), + RiskInstanceId = column_ifexists('RiskInstanceId', ''), + SupplyChainId = column_ifexists('SupplyChainId', ''), + AssetType = column_ifexists('AssetType', ''), + RiskTypeId = column_ifexists('RiskTypeId', ''), + CveId = column_ifexists('CveId', ''), + RiskCategory = column_ifexists('RiskCategory', ''), + RiskName = column_ifexists('RiskName', ''), + Description = column_ifexists('Description', ''), + Remediations = column_ifexists('Remediations', dynamic(null)), + Severity = column_ifexists('Severity', ''), + Score = column_ifexists('Score', real(null)), + Confidence = column_ifexists('Confidence', real(null)), + Status = column_ifexists('Status', ''), + ResolveTime = column_ifexists('ResolveTime', datetime(null)), + ResolveReason = column_ifexists('ResolveReason', ''), + SupplierId = column_ifexists('SupplierId', ''), + SupplierDisplayName = column_ifexists('SupplierDisplayName', ''), + Suppliers = column_ifexists('Suppliers', dynamic(null)), + AssetIp = column_ifexists('AssetIp', ''), + AssetHostname = column_ifexists('AssetHostname', ''), + AssetPort = column_ifexists('AssetPort', int(null)), + ServiceTransportProtocol = column_ifexists('ServiceTransportProtocol', ''), + ServicePort = column_ifexists('ServicePort', int(null)), + ServiceProtocol = column_ifexists('ServiceProtocol', ''), + Tags = column_ifexists('Tags', dynamic(null)), + RapidResponse = column_ifexists('RapidResponse', bool(null)), + Evidence = column_ifexists('Evidence', ''), + FirstDetectedAt = column_ifexists('FirstDetectedAt', datetime(null)), + LastDetectedAt = column_ifexists('LastDetectedAt', datetime(null)), + CreatedAt = column_ifexists('CreatedAt', datetime(null)), + PlatformUrl = column_ifexists('PlatformUrl', '') + // Delivery is at-least-once, so keep one row per event: the first copy that was ingested + | summarize arg_min(IngestedAt, *) by EventId + | extend + EventVendor = "Censys", + EventProduct = "Censys Supplier Risk", + ChangeType = tolower(ChangeType), + Status = tolower(Status), + Severity = toupper(Severity), + SupplierId = coalesce(tostring(SupplierId), tostring(Suppliers[0].id)), + SupplierDisplayName = coalesce(tostring(SupplierDisplayName), tostring(Suppliers[0].display_name)), + AssetName = iff(isnotempty(AssetHostname), AssetHostname, AssetIp), + IsOpen = tolower(Status) == "open", + SeverityRank = case( + toupper(Severity) == "CRITICAL", 4, + toupper(Severity) == "HIGH", 3, + toupper(Severity) == "MEDIUM", 2, + toupper(Severity) == "LOW", 1, + 0) + | project + TimeGenerated, + IngestedAt, + EventId, + InsertedAt, + OccurredAt, + ChangeType, + RiskInstanceId, + SupplyChainId, + RiskTypeId, + CveId, + RiskCategory, + RiskName, + Description, + Remediations, + Severity, + SeverityRank, + Score, + Confidence, + Status, + IsOpen, + ResolveTime, + ResolveReason, + SupplierId, + SupplierDisplayName, + Suppliers, + AssetType, + AssetName, + AssetIp, + AssetHostname, + AssetPort, + ServiceTransportProtocol, + ServicePort, + ServiceProtocol, + Tags, + RapidResponse, + Evidence, + FirstDetectedAt, + LastDetectedAt, + CreatedAt, + PlatformUrl, + EventVendor, + EventProduct + }; + CensysRiskEvents_view diff --git a/Solutions/Censys Supplier Risk/Parsers/CensysRisks.yaml b/Solutions/Censys Supplier Risk/Parsers/CensysRisks.yaml new file mode 100644 index 00000000000..30043f8a6f8 --- /dev/null +++ b/Solutions/Censys Supplier Risk/Parsers/CensysRisks.yaml @@ -0,0 +1,15 @@ +id: 821035be-713b-4be9-a568-87b8b0b62f41 +Function: + Title: Parser for Censys risks + Version: "1.0.0" + LastUpdated: "2026-10-21" +Category: Microsoft Sentinel Parser +FunctionName: CensysRisks +FunctionAlias: CensysRisks +FunctionQuery: | + // Current state of every supplier risk: the latest event of each risk instance, whether the risk is open or closed + let CensysRisks_view = view () { + CensysRiskEvents + | summarize arg_max(InsertedAt, *) by RiskInstanceId + }; + CensysRisks_view diff --git a/Solutions/Censys Supplier Risk/ReleaseNotes.md b/Solutions/Censys Supplier Risk/ReleaseNotes.md new file mode 100644 index 00000000000..56d45fefe7d --- /dev/null +++ b/Solutions/Censys Supplier Risk/ReleaseNotes.md @@ -0,0 +1,3 @@ +| **Version** | **Date Modified (DD-MM-YYYY)** | **Change History** | +|-------------|--------------------------------|---------------------------------------------| +| 3.0.0 | 29-10-2026 | Initial Solution Release | diff --git a/Solutions/Censys Supplier Risk/SolutionMetadata.json b/Solutions/Censys Supplier Risk/SolutionMetadata.json new file mode 100644 index 00000000000..ef692c69da8 --- /dev/null +++ b/Solutions/Censys Supplier Risk/SolutionMetadata.json @@ -0,0 +1,17 @@ +{ + "publisherId": "censys", + "offerId": "censys_supplier_risk", + "firstPublishDate": "2026-10-29", + "lastPublishDate": "2026-10-29", + "providers": ["Censys"], + "categories": { + "domains": ["Security - Network", "Security - Cloud Security"], + "verticals": [] + }, + "support": { + "name": "Censys Support", + "email": "support@censys.com", + "tier": "Partner", + "link": "https://docs.censys.com" + } +} \ No newline at end of file diff --git a/Solutions/Censys Supplier Risk/Workbooks/CensysSupplierRisk.json b/Solutions/Censys Supplier Risk/Workbooks/CensysSupplierRisk.json new file mode 100644 index 00000000000..5fd3f6cfdfb --- /dev/null +++ b/Solutions/Censys Supplier Risk/Workbooks/CensysSupplierRisk.json @@ -0,0 +1,1097 @@ +{ + "version": "Notebook/1.0", + "items": [ + { + "type": 1, + "content": { + "json": "## Censys Supplier Risk Overview\nOverview dashboard for Censys Supplier Risk risk events. All panels count risk events in the selected **Time Range**, one per event id, and every filter applies to every panel. Open counts use the status that each event carries, so a risk opened earlier than the Time Range with no event inside it will not appear." + }, + "name": "0a3c227f-21b3-55b9-b3d4-6c6d84922765" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "3980b3ca-3e47-5259-ae54-e9cf79f7aa97", + "version": "KqlParameterItem/1.0", + "name": "TimeRange", + "label": "Time Range", + "type": 4, + "isRequired": true, + "value": { + "durationMs": 604800000 + }, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 86400000 + }, + { + "durationMs": 259200000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 1209600000 + }, + { + "durationMs": 2592000000 + } + ], + "allowCustom": true + } + }, + { + "id": "531ff72e-136a-5683-8495-98755dec1185", + "version": "KqlParameterItem/1.0", + "name": "Severity", + "label": "Severity", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "timeContext": { + "durationMs": 7776000000 + }, + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "selectAllValue": "*", + "showDefault": false + }, + "defaultValue": "value::all", + "value": [ + "value::all" + ], + "jsonData": "[{\"value\": \"LOW\", \"label\": \"Low\"}, {\"value\": \"MEDIUM\", \"label\": \"Medium\"}, {\"value\": \"HIGH\", \"label\": \"High\"}, {\"value\": \"CRITICAL\", \"label\": \"Critical\"}]" + }, + { + "id": "408ed010-0624-5802-be49-0b16ce3b0b55", + "version": "KqlParameterItem/1.0", + "name": "Supplier", + "label": "Supplier", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "CensysRiskEvents\n| where isnotempty(SupplierDisplayName)\n| summarize by SupplierDisplayName\n| project value = SupplierDisplayName, label = SupplierDisplayName\n| order by label asc", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "timeContext": { + "durationMs": 7776000000 + }, + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "selectAllValue": "*", + "showDefault": false + }, + "defaultValue": "value::all", + "value": [ + "value::all" + ] + }, + { + "id": "981f1274-a745-5685-9c0a-938042ba7b06", + "version": "KqlParameterItem/1.0", + "name": "RiskCategory", + "label": "Risk Category", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "timeContext": { + "durationMs": 7776000000 + }, + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "selectAllValue": "*", + "showDefault": false + }, + "defaultValue": "value::all", + "value": [ + "value::all" + ], + "jsonData": "[{\"value\": \"VULNERABILITY\", \"label\": \"Vulnerability\"}, {\"value\": \"MISCONFIGURATION\", \"label\": \"Misconfiguration\"}, {\"value\": \"EXPOSURE\", \"label\": \"Exposure\"}, {\"value\": \"THREAT\", \"label\": \"Threat\"}]" + }, + { + "id": "2a6c8d3e-7b1f-5f0e-9a44-3c2d9e8b1a77", + "version": "KqlParameterItem/1.0", + "name": "Status", + "label": "Status", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "timeContext": { + "durationMs": 7776000000 + }, + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "selectAllValue": "*", + "showDefault": false + }, + "defaultValue": "value::all", + "value": [ + "value::all" + ], + "jsonData": "[{\"value\": \"open\", \"label\": \"Open\"}, {\"value\": \"closed\", \"label\": \"Closed\"}]" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "d242da61-ec90-533c-946e-20debc8b18b2" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "b0aac801-3580-5807-8df7-7a5e49f3c503", + "version": "KqlParameterItem/1.0", + "name": "P1Severity", + "type": 1, + "isRequired": false, + "isHiddenWhenLocked": true, + "value": "None" + }, + { + "id": "9952c2a7-5e8b-5f2f-a19d-dc9d340ca4fe", + "version": "KqlParameterItem/1.0", + "name": "P2Date", + "type": 1, + "isRequired": false, + "isHiddenWhenLocked": true, + "value": "None" + }, + { + "id": "c97c3004-6c39-55d2-8b8a-a3c41ef751f5", + "version": "KqlParameterItem/1.0", + "name": "P2Status", + "type": 1, + "isRequired": false, + "isHiddenWhenLocked": true, + "value": "None" + }, + { + "id": "c070c984-7e2c-568c-8d8f-b6ca8ffaffd8", + "version": "KqlParameterItem/1.0", + "name": "P3Category", + "type": 1, + "isRequired": false, + "isHiddenWhenLocked": true, + "value": "None" + }, + { + "id": "b5d774c8-0c6d-5ffa-8e74-193a9f8dcd6e", + "version": "KqlParameterItem/1.0", + "name": "P4Risk", + "type": 1, + "isRequired": false, + "isHiddenWhenLocked": true, + "value": "None" + }, + { + "id": "6179b664-6feb-5562-9189-6351e12c6713", + "version": "KqlParameterItem/1.0", + "name": "P5Supplier", + "type": 1, + "isRequired": false, + "isHiddenWhenLocked": true, + "value": "None" + }, + { + "id": "c728699c-76e3-5dd5-b847-ed58b40037b7", + "version": "KqlParameterItem/1.0", + "name": "P6Event", + "type": 1, + "isRequired": false, + "isHiddenWhenLocked": true, + "value": "None" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "383c127f-e50f-5a35-8def-1a9f9987899b" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "CensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| where Status == \"open\"\n| summarize [\"count\"] = count() by severity = Severity\n| order by severity asc", + "size": 3, + "title": "Open Risk Count by Severity", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "piechart", + "exportFieldName": "series", + "exportParameterName": "P1Severity", + "exportDefaultValue": "None", + "chartSettings": { + "group": "severity", + "seriesLabelSettings": [ + { + "seriesName": "CRITICAL", + "color": "redBright" + }, + { + "seriesName": "HIGH", + "color": "orange" + }, + { + "seriesName": "MEDIUM", + "color": "yellow" + }, + { + "seriesName": "LOW", + "color": "blue" + }, + { + "seriesName": "UNSPECIFIED", + "color": "gray" + } + ], + "ySettings": { + "numberFormatSettings": { + "unit": 0, + "options": { + "style": "decimal", + "useGrouping": true, + "maximumSignificantDigits": 15 + } + } + } + } + }, + "name": "1b6bdd62-e666-568e-ad83-e6466e7d0772", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "CensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| summarize [\"count\"] = count() by category = RiskCategory\n| order by category asc", + "size": 3, + "title": "Risk Events by Category", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "piechart", + "chartSettings": { + "group": "category", + "ySettings": { + "numberFormatSettings": { + "unit": 0, + "options": { + "style": "decimal", + "useGrouping": true, + "maximumSignificantDigits": 15 + } + } + } + }, + "exportFieldName": "series", + "exportParameterName": "P3Category", + "exportDefaultValue": "None" + }, + "name": "ab26b647-f261-532b-937b-fc06058cd3f2", + "customWidth": "50" + }, + { + "type": 1, + "content": { + "json": "\ud83d\udca1 _Click on a slice in the above 'Open Risk Count by Severity' chart to view more details_\n\n\ud83d\udcdd _Counts open risk events by severity, one per event id._" + }, + "name": "3408c688-c583-5070-b687-a6b6c057b141", + "customWidth": "50" + }, + { + "type": 1, + "content": { + "json": "\ud83d\udca1 _Click on a slice in the above 'Risk Events by Category' chart to view more details_\n\n\ud83d\udcdd _Counts risk events by category, one per event id._" + }, + "name": "a91116af-f6ff-5304-8e46-14afb20ca027", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "CensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| where Status == \"open\"\n| where Severity == '{P1Severity}'\n| order by InsertedAt desc\n| project Time = InsertedAt, id = EventId, [\"Risk Name\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\"Platform URL\"] = PlatformUrl\n| take 10000", + "size": 0, + "title": "Events - {P1Severity}", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Time", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "id", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20%" + } + }, + { + "columnMatch": "Risk Name", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "27%" + } + }, + { + "columnMatch": "Severity", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "8%" + } + }, + { + "columnMatch": "Supplier", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "Platform URL", + "formatter": 7, + "formatOptions": { + "linkTarget": "Url", + "linkLabel": "Open in Censys", + "customColumnWidthSetting": "15%" + } + } + ], + "filter": true, + "rowLimit": 10000 + } + }, + "name": "b4afc41f-40bd-50d8-a9f6-66f8d2b30133", + "customWidth": "100", + "conditionalVisibility": { + "parameterName": "P1Severity", + "comparison": "isNotEqualTo", + "value": "None" + } + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "CensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| where RiskCategory == '{P3Category}'\n| order by InsertedAt desc\n| project Time = InsertedAt, id = EventId, [\"Risk Name\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\"Platform URL\"] = PlatformUrl\n| take 10000", + "size": 0, + "title": "Events - {P3Category}", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Time", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "id", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20%" + } + }, + { + "columnMatch": "Risk Name", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "27%" + } + }, + { + "columnMatch": "Severity", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "8%" + } + }, + { + "columnMatch": "Supplier", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "Platform URL", + "formatter": 7, + "formatOptions": { + "linkTarget": "Url", + "linkLabel": "Open in Censys", + "customColumnWidthSetting": "15%" + } + } + ], + "filter": true, + "rowLimit": 10000 + } + }, + "name": "70e7cbe8-0bc7-571e-81e4-7d792f321c2b", + "customWidth": "100", + "conditionalVisibility": { + "parameterName": "P3Category", + "comparison": "isNotEqualTo", + "value": "None" + } + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let TotalHours = (todatetime({TimeRange:end}) - todatetime({TimeRange:start})) / 1h;\n// Time span picked from the Time Range (at most about 100 points): 30 minutes, 1 hour or 1 day\nlet Span = iff(TotalHours <= 50, 30m, iff(TotalHours <= 100, 1h, 1d));\nlet Buckets = range Bucket from bin(todatetime({TimeRange:start}), Span) to todatetime({TimeRange:end}) step Span | extend k = 1;\nlet Statuses = datatable(Series: string)[\"Opened\", \"Closed\"] | extend k = 1;\nlet Counts = CensysRiskEvents\n | where OccurredAt {TimeRange}\n | where ('*' in ({Status}) or Status in ({Status}))\n | where ('*' in ({Severity}) or Severity in ({Severity}))\n | where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n | where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n | extend Series = case(Status == \"open\", \"Opened\", Status == \"closed\", \"Closed\", Status)\n | summarize Total = count() by Bucket = bin(OccurredAt, Span), Series;\nBuckets\n| join kind=inner Statuses on k\n| join kind=leftouter Counts on Bucket, Series\n| project Time = iff(Span >= 1d, format_datetime(Bucket, 'yyyy-MM-dd'), format_datetime(Bucket, 'yyyy-MM-dd HH:mm')), Series, [\"Risk Count\"] = coalesce(Total, 0)\n| order by Time asc, Series asc", + "size": 0, + "title": "Opened vs. Closed Risks", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "timechart", + "chartSettings": { + "xAxis": "Time", + "group": "Series", + "xSettings": { + "label": "Time" + }, + "ySettings": { + "label": "Risk Count" + } + }, + "exportedParameters": [ + { + "fieldName": "x", + "parameterName": "P2Date", + "parameterType": 1, + "defaultValue": "None" + }, + { + "fieldName": "series", + "parameterName": "P2Status", + "parameterType": 1, + "defaultValue": "None" + } + ] + }, + "name": "4668c9cb-64a5-581d-b461-d8854d0f2409", + "customWidth": "100" + }, + { + "type": 1, + "content": { + "json": "\ud83d\udca1 _Click on a data point in the above 'Opened vs. Closed Risks' chart to view more details_\n\n\ud83d\udcdd _Counts risk events per day by their status (Opened or Closed), by when they occurred._" + }, + "name": "c2beca44-a16d-5fa8-821b-d7aea664ec9f", + "customWidth": "100" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let TotalHours = (todatetime({TimeRange:end}) - todatetime({TimeRange:start})) / 1h;\n// Time span picked from the Time Range (at most about 100 points): 30 minutes, 1 hour or 1 day\nlet Span = iff(TotalHours <= 50, 30m, iff(TotalHours <= 100, 1h, 1d));\nCensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| where Status == iff('{P2Status}' == \"Opened\", \"open\", \"closed\")\n| where OccurredAt >= todatetime(substring('{P2Date}', 0, 16)) and OccurredAt < todatetime(substring('{P2Date}', 0, 16)) + Span\n| order by InsertedAt desc\n| project Time = InsertedAt, id = EventId, [\"Risk Name\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\"Platform URL\"] = PlatformUrl\n| take 10000", + "size": 0, + "title": "Events - {P2Status} on {P2Date}", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Time", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "id", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20%" + } + }, + { + "columnMatch": "Risk Name", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "27%" + } + }, + { + "columnMatch": "Severity", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "8%" + } + }, + { + "columnMatch": "Supplier", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "Platform URL", + "formatter": 7, + "formatOptions": { + "linkTarget": "Url", + "linkLabel": "Open in Censys", + "customColumnWidthSetting": "15%" + } + } + ], + "filter": true, + "rowLimit": 10000 + } + }, + "name": "50857b22-679b-5891-bd85-96262bd29d80", + "customWidth": "100", + "conditionalVisibilities": [ + { + "parameterName": "P2Date", + "comparison": "isNotEqualTo", + "value": "None" + }, + { + "parameterName": "P2Status", + "comparison": "isNotEqualTo", + "value": "None" + } + ] + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "CensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| where Status == \"open\" and isnotempty(RiskName)\n| summarize OpenRisks = count() by [\"Risk Name\"] = RiskName\n| order by OpenRisks desc\n| take 10", + "size": 0, + "title": "Top 10 Risks by Open Risk Count", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Risk Name", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "80%" + } + }, + { + "columnMatch": "OpenRisks", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20%" + } + } + ], + "filter": true, + "rowLimit": 10000 + }, + "exportFieldName": "Risk Name", + "exportParameterName": "P4Risk", + "exportDefaultValue": "None" + }, + "name": "4c44d1fa-a2d9-55b0-98e5-8030350c5f9d", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "CensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| where Status == \"open\"\n| summarize OpenRisks = count() by Supplier = SupplierDisplayName\n| order by OpenRisks desc", + "size": 0, + "title": "Supplier Risk Ranking", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Supplier", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "80%" + } + }, + { + "columnMatch": "OpenRisks", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20%" + } + } + ], + "filter": true, + "rowLimit": 10000 + }, + "exportFieldName": "Supplier", + "exportParameterName": "P5Supplier", + "exportDefaultValue": "None" + }, + "name": "a5821846-0945-540c-9c97-849cad7afa8b", + "customWidth": "50" + }, + { + "type": 1, + "content": { + "json": "\ud83d\udca1 _Click on a row in the above 'Top 10 Risks by Open Risk Count' grid to view more details_\n\n\ud83d\udcdd _The 10 risk names with the most open risk events._" + }, + "name": "cdf92177-e783-5d83-97d1-37d122085500", + "customWidth": "50" + }, + { + "type": 1, + "content": { + "json": "\ud83d\udca1 _Click on a row in the above 'Supplier Risk Ranking' grid to view more details_\n\n\ud83d\udcdd _Suppliers ranked by their number of open risk events._" + }, + "name": "4e9590ae-6386-5f24-9ca0-f3a096994918", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "CensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| where Status == \"open\"\n| where RiskName == '{P4Risk}'\n| order by InsertedAt desc\n| project Time = InsertedAt, id = EventId, [\"Risk Name\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\"Platform URL\"] = PlatformUrl\n| take 10000", + "size": 0, + "title": "Events - {P4Risk}", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Time", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "id", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20%" + } + }, + { + "columnMatch": "Risk Name", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "27%" + } + }, + { + "columnMatch": "Severity", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "8%" + } + }, + { + "columnMatch": "Supplier", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "Platform URL", + "formatter": 7, + "formatOptions": { + "linkTarget": "Url", + "linkLabel": "Open in Censys", + "customColumnWidthSetting": "15%" + } + } + ], + "filter": true, + "rowLimit": 10000 + } + }, + "name": "22004c2d-0bf3-5e3d-9870-78807252bd52", + "customWidth": "100", + "conditionalVisibility": { + "parameterName": "P4Risk", + "comparison": "isNotEqualTo", + "value": "None" + } + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "CensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| where Status == \"open\"\n| where SupplierDisplayName == '{P5Supplier}'\n| order by InsertedAt desc\n| project Time = InsertedAt, id = EventId, [\"Risk Name\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\"Platform URL\"] = PlatformUrl\n| take 10000", + "size": 0, + "title": "Events - {P5Supplier}", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Time", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "id", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20%" + } + }, + { + "columnMatch": "Risk Name", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "27%" + } + }, + { + "columnMatch": "Severity", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "8%" + } + }, + { + "columnMatch": "Supplier", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "Platform URL", + "formatter": 7, + "formatOptions": { + "linkTarget": "Url", + "linkLabel": "Open in Censys", + "customColumnWidthSetting": "15%" + } + } + ], + "filter": true, + "rowLimit": 10000 + } + }, + "name": "67930e57-64b3-52a9-b227-bebac1f40c88", + "customWidth": "100", + "conditionalVisibility": { + "parameterName": "P5Supplier", + "comparison": "isNotEqualTo", + "value": "None" + } + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "CensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| where RapidResponse == true\n| order by InsertedAt desc\n| project Time = InsertedAt, id = EventId, [\"Risk Name\"] = RiskName, Severity, Supplier = SupplierDisplayName, [\"Platform URL\"] = PlatformUrl\n| take 10000", + "size": 0, + "title": "Rapid Response Events", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Time", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "id", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20%" + } + }, + { + "columnMatch": "Risk Name", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "27%" + } + }, + { + "columnMatch": "Severity", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "8%" + } + }, + { + "columnMatch": "Supplier", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15%" + } + }, + { + "columnMatch": "Platform URL", + "formatter": 7, + "formatOptions": { + "linkTarget": "Url", + "linkLabel": "Open in Censys", + "customColumnWidthSetting": "15%" + } + } + ], + "filter": true, + "rowLimit": 10000 + }, + "exportFieldName": "id", + "exportParameterName": "P6Event", + "exportDefaultValue": "None" + }, + "name": "d29db818-909d-5958-8ef4-89e50ee80542", + "customWidth": "100" + }, + { + "type": 1, + "content": { + "json": "\ud83d\udca1 _Click on a row in the above 'Rapid Response Events' grid to view more details_\n\n\ud83d\udcdd _Risk events flagged for rapid response (actively exploited), newest first._" + }, + "name": "59f191f6-06f8-5ad8-b72b-8394af1f9a2a", + "customWidth": "100" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "CensysRiskEvents\n| where OccurredAt {TimeRange}\n| where ('*' in ({Status}) or Status in ({Status}))\n| where ('*' in ({Severity}) or Severity in ({Severity}))\n| where ('*' in ({Supplier}) or SupplierDisplayName in ({Supplier}))\n| where ('*' in ({RiskCategory}) or RiskCategory in ({RiskCategory}))\n| where EventId == '{P6Event}'\n| project Time = InsertedAt, id = EventId, [\"Change Type\"] = ChangeType, Status, [\"Risk Name\"] = RiskName, Severity, Supplier = SupplierDisplayName, Asset = AssetName, [\"Service Port\"] = ServicePort, [\"Resolve Reason\"] = ResolveReason, [\"Platform URL\"] = PlatformUrl\n| take 10000", + "size": 0, + "title": "Event - {P6Event}", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "showRefreshButton": true, + "showAnalytics": true, + "showExportToExcel": true, + "noDataMessage": "No data found for the selected filters.", + "timeContext": { + "durationMs": 7776000000 + }, + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Time", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "12%" + } + }, + { + "columnMatch": "id", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "16%" + } + }, + { + "columnMatch": "Change Type", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "8%" + } + }, + { + "columnMatch": "Status", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "7%" + } + }, + { + "columnMatch": "Risk Name", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20%" + } + }, + { + "columnMatch": "Severity", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "7%" + } + }, + { + "columnMatch": "Supplier", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "11%" + } + }, + { + "columnMatch": "Asset", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "Service Port", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "4%" + } + }, + { + "columnMatch": "Resolve Reason", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "5%" + } + }, + { + "columnMatch": "Platform URL", + "formatter": 7, + "formatOptions": { + "linkTarget": "Url", + "linkLabel": "Open in Censys", + "customColumnWidthSetting": "10%" + } + } + ], + "filter": true, + "rowLimit": 10000 + } + }, + "name": "996c3d06-11cb-52d7-86a5-2a6444795041", + "customWidth": "100", + "conditionalVisibility": { + "parameterName": "P6Event", + "comparison": "isNotEqualTo", + "value": "None" + } + } + ], + "fallbackResourceIds": [], + "fromTemplateId": "sentinel-CensysSupplierRisk", + "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" +} diff --git a/Workbooks/Images/Preview/CensysSupplierRiskBlack1.png b/Workbooks/Images/Preview/CensysSupplierRiskBlack1.png new file mode 100644 index 00000000000..8ae03395b1c Binary files /dev/null and b/Workbooks/Images/Preview/CensysSupplierRiskBlack1.png differ diff --git a/Workbooks/Images/Preview/CensysSupplierRiskWhite1.png b/Workbooks/Images/Preview/CensysSupplierRiskWhite1.png new file mode 100644 index 00000000000..17905354d84 Binary files /dev/null and b/Workbooks/Images/Preview/CensysSupplierRiskWhite1.png differ diff --git a/Workbooks/WorkbooksMetadata.json b/Workbooks/WorkbooksMetadata.json index 35d2aa0831e..faf7debc706 100644 --- a/Workbooks/WorkbooksMetadata.json +++ b/Workbooks/WorkbooksMetadata.json @@ -11614,6 +11614,26 @@ "subtitle": "", "provider": "PRODAFT" }, + { + "workbookKey": "CensysSupplierRiskWorkbook", + "logoFileName": "Censys.svg", + "description": "Gain insight into supplier risk collected from Censys Supplier Risk: open risks by severity, risk events by category, opened and closed risks over time, top risks by open risk count, supplier risk ranking and rapid response events, with details one selection away.", + "dataTypesDependencies": [ + "CensysRiskEvents_CL" + ], + "dataConnectorsDependencies": [ + "CensysSRRiskEventsFunction" + ], + "previewImagesFileNames": [ + "CensysSupplierRiskWhite1.png", + "CensysSupplierRiskBlack1.png" + ], + "version": "1.0.0", + "title": "Censys Supplier Risk", + "templateRelativePath": "CensysSupplierRisk.json", + "subtitle": "", + "provider": "Censys" + }, { "workbookKey": "DelineaIdentityAuthOverview", "logoFileName": "DelineaPlatformLogo.svg",