mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-03 17:54:05 +00:00
feat(compliance): FedRAMP 20x Class C FRR + AWS checks (#12808)
This commit is contained in:
@@ -126,12 +126,12 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/user-guide/compliance/tutorials/compliance) | [Categories](https://docs.prowler.com/user-guide/cli/tutorials/misc#categories) | Support | Interface |
|
||||
|---|---|---|---|---|---|---|
|
||||
| AWS | 639 | 86 | 47 | 19 | Official | UI, API, CLI |
|
||||
| Azure | 191 | 22 | 21 | 16 | Official | UI, API, CLI |
|
||||
| GCP | 109 | 20 | 19 | 12 | Official | UI, API, CLI |
|
||||
| Kubernetes | 92 | 7 | 8 | 11 | Official | UI, API, CLI |
|
||||
| AWS | 662 | 86 | 50 | 19 | Official | UI, API, CLI |
|
||||
| Azure | 191 | 22 | 25 | 16 | Official | UI, API, CLI |
|
||||
| GCP | 110 | 20 | 22 | 12 | Official | UI, API, CLI |
|
||||
| Kubernetes | 92 | 7 | 11 | 11 | Official | UI, API, CLI |
|
||||
| GitHub | 24 | 3 | 2 | 5 | Official | UI, API, CLI |
|
||||
| M365 | 143 | 10 | 6 | 10 | Official | UI, API, CLI |
|
||||
| M365 | 144 | 10 | 9 | 10 | Official | UI, API, CLI |
|
||||
| OCI | 52 | 14 | 5 | 10 | Official | UI, API, CLI |
|
||||
| Alibaba Cloud | 63 | 9 | 6 | 9 | Official | UI, API, CLI |
|
||||
| Cloudflare | 29 | 3 | 2 | 5 | Official | UI, API, CLI |
|
||||
|
||||
@@ -212,6 +212,14 @@ mainConfig:
|
||||
# MEDIUM
|
||||
ecr_repository_vulnerability_minimum_severity: "MEDIUM"
|
||||
|
||||
# AWS Inspector2
|
||||
# aws.inspector2_coverage_recently_scanned
|
||||
# Maximum days since Inspector2 last scanned an actively covered resource
|
||||
inspector2_max_days_since_last_scan: 3
|
||||
# aws.inspector2_active_findings_within_max_age
|
||||
# Maximum days an Inspector2 finding can stay active since it was first observed
|
||||
inspector2_active_finding_max_age_days: 192
|
||||
|
||||
# AWS Trusted Advisor
|
||||
# aws.trustedadvisor_premium_support_plan_subscribed
|
||||
verify_premium_support_plans: True
|
||||
|
||||
@@ -154,6 +154,8 @@ Only fields with a numeric range, a fixed value set, or a length cap are listed.
|
||||
| `max_days_secret_unused` | `7..365` days | |
|
||||
| `max_days_secret_unrotated` | `1..180` days | NIST IA-5: rotate quarterly; CIS ≤90 |
|
||||
| `min_kinesis_stream_retention_hours` | `24..8760` h | 1 day .. 1 year |
|
||||
| `inspector2_max_days_since_last_scan` | `1..90` days | |
|
||||
| `inspector2_active_finding_max_age_days` | `1..365` days | Default `192` matches the FedRAMP 20x rule that marks vulnerabilities still open after 192 days as accepted |
|
||||
| `shodan_api_key` | ≤512 chars | |
|
||||
|
||||
### Azure
|
||||
|
||||
@@ -91,6 +91,8 @@ The following list includes all the AWS checks with configurable variables that
|
||||
| `iam_user_access_not_stale_to_sagemaker` | `max_unused_sagemaker_access_days` | Integer | `90` |
|
||||
| `iam_user_accesskey_unused` | `max_unused_access_keys_days` | Integer | `45` |
|
||||
| `iam_user_console_access_unused` | `max_console_access_days` | Integer | `45` |
|
||||
| `inspector2_active_findings_within_max_age` | `inspector2_active_finding_max_age_days` | Integer | `192` |
|
||||
| `inspector2_coverage_recently_scanned` | `inspector2_max_days_since_last_scan` | Integer | `3` |
|
||||
| `kinesis_stream_data_retention_period` | `min_kinesis_stream_retention_hours` | Integer | `168` |
|
||||
| `neptune_cluster_backup_enabled` | `minimum_backup_retention_period` | Integer | `7` |
|
||||
| `opensearch_service_domains_not_publicly_accessible` | `trusted_ips` | List of Strings | `[]` |
|
||||
@@ -490,6 +492,14 @@ aws:
|
||||
# MEDIUM
|
||||
ecr_repository_vulnerability_minimum_severity: "MEDIUM"
|
||||
|
||||
# AWS Inspector2
|
||||
# aws.inspector2_coverage_recently_scanned
|
||||
# Maximum days since Inspector2 last scanned an actively covered resource
|
||||
inspector2_max_days_since_last_scan: 3
|
||||
# aws.inspector2_active_findings_within_max_age
|
||||
# Maximum days an Inspector2 finding can stay active since it was first observed
|
||||
inspector2_active_finding_max_age_days: 192
|
||||
|
||||
# AWS Trusted Advisor
|
||||
# aws.trustedadvisor_premium_support_plan_subscribed
|
||||
verify_premium_support_plans: True
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"glue:GetSecurityConfiguration*",
|
||||
"glue:SearchTables",
|
||||
"glue:GetMLTransforms",
|
||||
"inspector2:BatchGetFindingDetails",
|
||||
"lambda:GetFunction*",
|
||||
"lambda:GetLayerVersion",
|
||||
"logs:FilterLogEvents",
|
||||
|
||||
@@ -210,6 +210,7 @@ Resources:
|
||||
- "glue:GetSecurityConfiguration*"
|
||||
- "glue:SearchTables"
|
||||
- "glue:GetMLTransforms"
|
||||
- "inspector2:BatchGetFindingDetails"
|
||||
- "lambda:GetFunction*"
|
||||
- "logs:FilterLogEvents"
|
||||
- "lightsail:GetRelationalDatabases"
|
||||
@@ -479,6 +480,7 @@ Resources:
|
||||
- "glue:GetSecurityConfiguration*"
|
||||
- "glue:SearchTables"
|
||||
- "glue:GetMLTransforms"
|
||||
- "inspector2:BatchGetFindingDetails"
|
||||
- "lambda:GetFunction*"
|
||||
- "logs:FilterLogEvents"
|
||||
- "lightsail:GetRelationalDatabases"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
`inspector2_coverage_scan_status_active`, `inspector2_coverage_recently_scanned`, `inspector2_active_findings_no_known_exploited_vulnerabilities`, `inspector2_active_findings_kev_within_due_date`, `inspector2_active_findings_within_max_age`, `elbv2_listener_fips_tls_enabled` and `transfer_server_fips_security_policy_enabled` checks for AWS provider, covering FedRAMP 20x Class C vulnerability detection, CISA KEV remediation and FIPS cryptography rules; the KEV checks require `inspector2:BatchGetFindingDetails`, now in the Prowler additions policy
|
||||
@@ -0,0 +1 @@
|
||||
`FedRAMP-20x-FRR-Class-C` universal compliance framework (`fedramp_20x_frr_class_c_2026`) with the 158 provider rules of the FedRAMP 20x Class C ruleset from the FedRAMP Consolidated Rules 2026 for AWS, Azure, GCP, Kubernetes and M365
|
||||
File diff suppressed because it is too large
Load Diff
@@ -190,6 +190,14 @@ aws:
|
||||
# MEDIUM
|
||||
ecr_repository_vulnerability_minimum_severity: "MEDIUM"
|
||||
|
||||
# AWS Inspector2
|
||||
# aws.inspector2_coverage_recently_scanned
|
||||
# Maximum days since Inspector2 last scanned an actively covered resource
|
||||
inspector2_max_days_since_last_scan: 3
|
||||
# aws.inspector2_active_findings_within_max_age
|
||||
# Maximum days an Inspector2 finding can stay active since it was first observed
|
||||
inspector2_active_finding_max_age_days: 192
|
||||
|
||||
# AWS Trusted Advisor
|
||||
# aws.trustedadvisor_premium_support_plan_subscribed
|
||||
verify_premium_support_plans: True
|
||||
|
||||
@@ -333,6 +333,20 @@ class AWSProviderConfig(ProviderConfigBase):
|
||||
description="Highest severity tolerated for ECR images.",
|
||||
)
|
||||
|
||||
# --- Inspector2 -------------------------------------------------------
|
||||
inspector2_max_days_since_last_scan: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=90,
|
||||
description="Days since Inspector2 last scanned a covered resource. Range: 1..90.",
|
||||
)
|
||||
inspector2_active_finding_max_age_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=365,
|
||||
description="Days an Inspector2 finding can stay active since first observed. Range: 1..365.",
|
||||
)
|
||||
|
||||
# --- Trusted Advisor --------------------------------------------------
|
||||
verify_premium_support_plans: Optional[bool] = None
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "elbv2_listener_fips_tls_enabled",
|
||||
"CheckTitle": "ELBv2 HTTPS/TLS listeners use a FIPS TLS security policy",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "elbv2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "low",
|
||||
"ResourceType": "AwsElbv2LoadBalancer",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**ELBv2 HTTPS and TLS listeners** are assessed for use of a **FIPS** TLS security policy (`ELBSecurityPolicy-*-FIPS-*`). FIPS policies terminate TLS with the AWS-LC FIPS validated cryptographic module.",
|
||||
"Risk": "Listeners without a FIPS policy terminate TLS with cryptographic modules that are not FIPS 140 validated, which does not meet requirements to protect federal or regulated data with **NIST CMVP validated cryptography**.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/elasticloadbalancing/latest/application/describe-ssl-policies.html",
|
||||
"https://docs.aws.amazon.com/elasticloadbalancing/latest/network/describe-ssl-policies.html",
|
||||
"https://aws.amazon.com/compliance/fips/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws elbv2 modify-listener --listener-arn <listener_arn> --ssl-policy ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04",
|
||||
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::ElasticLoadBalancingV2::Listener\n Properties:\n LoadBalancerArn: <example_resource_arn>\n Protocol: HTTPS\n Port: 443\n DefaultActions:\n - Type: forward\n TargetGroupArn: <example_resource_arn>\n Certificates:\n - CertificateArn: <example_certificate_arn>\n SslPolicy: ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04 # FIX: uses a FIPS TLS policy\n```",
|
||||
"Other": "1. In the AWS Console, go to EC2 > Load Balancers\n2. Select the load balancer and open the Listeners tab\n3. Select each HTTPS/TLS listener and choose Edit\n4. Set Security policy to a FIPS policy such as ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04\n5. Save changes",
|
||||
"Terraform": "```hcl\nresource \"aws_lb_listener\" \"<example_resource_name>\" {\n load_balancer_arn = \"<example_resource_arn>\"\n port = 443\n protocol = \"HTTPS\"\n ssl_policy = \"ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04\" # FIX: FIPS TLS policy\n certificate_arn = \"<example_certificate_arn>\"\n\n default_action {\n type = \"forward\"\n target_group_arn = \"<example_resource_arn>\"\n }\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use a **FIPS** TLS security policy, such as `ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04`, on every HTTPS and TLS listener that carries federal or regulated data.",
|
||||
"Url": "https://hub.prowler.com/check/elbv2_listener_fips_tls_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"elbv2_insecure_ssl_ciphers",
|
||||
"elbv2_listener_pqc_tls_enabled"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.elbv2.elbv2_client import elbv2_client
|
||||
|
||||
|
||||
class elbv2_listener_fips_tls_enabled(Check):
|
||||
"""Ensure every ELBv2 HTTPS or TLS listener uses a FIPS TLS security policy."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report whether each load balancer terminates HTTPS/TLS with a FIPS policy."""
|
||||
findings = []
|
||||
for lb in elbv2_client.loadbalancersv2.values():
|
||||
if lb.listener_discovery_failed:
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=lb)
|
||||
tls_listeners = {
|
||||
listener_arn: listener
|
||||
for listener_arn, listener in lb.listeners.items()
|
||||
if listener.protocol in ("HTTPS", "TLS")
|
||||
}
|
||||
non_fips_listeners = [
|
||||
f"{listener.protocol}:{listener.port} ({listener_arn}) uses {listener.ssl_policy or '<none>'}"
|
||||
for listener_arn, listener in tls_listeners.items()
|
||||
if "FIPS" not in (listener.ssl_policy or "").split("-")
|
||||
]
|
||||
if not tls_listeners:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ELBv2 {lb.name} has no HTTPS/TLS listeners."
|
||||
elif non_fips_listeners:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"ELBv2 {lb.name} has HTTPS/TLS listeners without a FIPS TLS security policy: {', '.join(non_fips_listeners)}."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ELBv2 {lb.name} has all HTTPS/TLS listeners using a FIPS TLS security policy."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_active_findings_kev_within_due_date",
|
||||
"CheckTitle": "Inspector2 has no active findings for CISA Known Exploited Vulnerabilities past their remediation due date",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Vulnerabilities/CVE",
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "critical",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**Amazon Inspector** active findings for **CISA Known Exploited Vulnerabilities** are compared with the remediation due date (`dateDue`) that CISA assigns to each entry of the KEV catalog. Findings that are still active after that date are reported.\n\nThe result is reported per Region where Inspector is enabled.",
|
||||
"Risk": "CISA due dates reflect **active exploitation**. Missing them keeps exploited vulnerabilities open beyond the window CISA sets for federal agencies and shows that vulnerability response is not keeping pace with real threats.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
|
||||
"https://docs.aws.amazon.com/inspector/v2/APIReference/API_BatchGetFindingDetails.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the AWS Console, open Amazon Inspector > Findings\n2. Filter by Vulnerability ID for each overdue CVE reported by this check\n3. Patch or upgrade the affected packages, rebuild container images or update Lambda runtimes\n4. If a fix is not available, apply the mitigations listed in the CISA catalog entry\n5. Confirm the findings move to Closed",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Track every KEV finding against its **CISA due date** and remediate before it passes. When no fix exists yet, apply the vendor or CISA mitigations and document the residual risk.",
|
||||
"Url": "https://hub.prowler.com/check/inspector2_active_findings_kev_within_due_date"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"vulnerabilities"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"inspector2_active_findings_no_known_exploited_vulnerabilities"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.inspector2.inspector2_client import (
|
||||
inspector2_client,
|
||||
)
|
||||
from prowler.providers.aws.services.inspector2.lib.vulnerabilities import (
|
||||
summarize_vulnerabilities,
|
||||
)
|
||||
|
||||
|
||||
class inspector2_active_findings_kev_within_due_date(Check):
|
||||
"""Ensure active Inspector2 findings for CISA KEVs are not past their CISA due date."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report, per Region, whether any CISA KEV finding is past its remediation due date."""
|
||||
findings = []
|
||||
now = datetime.now(timezone.utc)
|
||||
known_exploited = inspector2_client.known_exploited_vulnerabilities
|
||||
lookup_failed = inspector2_client.vulnerability_lookup_failed
|
||||
for inspector in inspector2_client.inspectors:
|
||||
if inspector.status != "ENABLED":
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=inspector)
|
||||
if inspector.findings is None:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 findings could not be retrieved in region {inspector.region}; "
|
||||
"verify the inspector2:ListFindings permission."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
vulnerability_ids = {
|
||||
finding.vulnerability_id
|
||||
for finding in inspector.findings
|
||||
if finding.vulnerability_id
|
||||
}
|
||||
overdue = sorted(
|
||||
f"{vulnerability_id} (due {known_exploited[vulnerability_id].date_due.date().isoformat()})"
|
||||
for vulnerability_id in known_exploited.keys() & vulnerability_ids
|
||||
if known_exploited[vulnerability_id].date_due
|
||||
and known_exploited[vulnerability_id].date_due < now
|
||||
)
|
||||
unverified_ids = sorted(vulnerability_ids & lookup_failed)
|
||||
if overdue:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has active findings in region {inspector.region} for CISA "
|
||||
"Known Exploited Vulnerabilities past their remediation due date: "
|
||||
f"{summarize_vulnerabilities(overdue)}."
|
||||
)
|
||||
elif unverified_ids:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
"Inspector2 could not verify the CISA Known Exploited Vulnerabilities status of "
|
||||
f"{summarize_vulnerabilities(unverified_ids)} in region {inspector.region}; "
|
||||
"verify the inspector2:BatchGetFindingDetails permission."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has no active findings in region {inspector.region} for CISA "
|
||||
"Known Exploited Vulnerabilities past their remediation due date."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_active_findings_no_known_exploited_vulnerabilities",
|
||||
"CheckTitle": "Inspector2 has no active findings for CISA Known Exploited Vulnerabilities",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Vulnerabilities/CVE",
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**Amazon Inspector** active findings are cross-referenced with the **CISA Known Exploited Vulnerabilities (KEV)** catalog, using the CISA data that Inspector returns in the finding details (`BatchGetFindingDetails`) of each CVE.\n\nThe result is reported per Region where Inspector is enabled.",
|
||||
"Risk": "KEV entries are vulnerabilities **confirmed as exploited in the wild**. Workloads carrying them are prime targets for initial access and ransomware, enabling remote code execution, data exfiltration and lateral movement.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
|
||||
"https://docs.aws.amazon.com/inspector/v2/APIReference/API_BatchGetFindingDetails.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the AWS Console, open Amazon Inspector > Findings\n2. Filter by Vulnerability ID for each CVE reported by this check\n3. Patch or upgrade the affected packages, rebuild container images or update Lambda runtimes\n4. Confirm the findings move to Closed",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Remediate KEV findings before any other vulnerability: patch or upgrade the affected packages, rebuild and redeploy container images, and stop deploying new resources that carry **known exploited vulnerabilities**.",
|
||||
"Url": "https://hub.prowler.com/check/inspector2_active_findings_no_known_exploited_vulnerabilities"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"vulnerabilities"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"inspector2_active_findings_exist",
|
||||
"inspector2_active_findings_kev_within_due_date"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.inspector2.inspector2_client import (
|
||||
inspector2_client,
|
||||
)
|
||||
from prowler.providers.aws.services.inspector2.lib.vulnerabilities import (
|
||||
summarize_vulnerabilities,
|
||||
)
|
||||
|
||||
|
||||
class inspector2_active_findings_no_known_exploited_vulnerabilities(Check):
|
||||
"""Ensure no active Inspector2 finding is a CISA Known Exploited Vulnerability."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report, per Region, whether any active finding is a CISA Known Exploited Vulnerability."""
|
||||
findings = []
|
||||
known_exploited = inspector2_client.known_exploited_vulnerabilities
|
||||
lookup_failed = inspector2_client.vulnerability_lookup_failed
|
||||
for inspector in inspector2_client.inspectors:
|
||||
if inspector.status != "ENABLED":
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=inspector)
|
||||
if inspector.findings is None:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 findings could not be retrieved in region {inspector.region}; "
|
||||
"verify the inspector2:ListFindings permission."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
vulnerability_ids = {
|
||||
finding.vulnerability_id
|
||||
for finding in inspector.findings
|
||||
if finding.vulnerability_id
|
||||
}
|
||||
kev_ids = sorted(known_exploited.keys() & vulnerability_ids)
|
||||
unverified_ids = sorted(vulnerability_ids & lookup_failed)
|
||||
if kev_ids:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has active findings in region {inspector.region} for CISA "
|
||||
f"Known Exploited Vulnerabilities: {summarize_vulnerabilities(kev_ids)}."
|
||||
)
|
||||
elif unverified_ids:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
"Inspector2 could not verify the CISA Known Exploited Vulnerabilities status of "
|
||||
f"{summarize_vulnerabilities(unverified_ids)} in region {inspector.region}; "
|
||||
"verify the inspector2:BatchGetFindingDetails permission."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has no active findings in region {inspector.region} for CISA "
|
||||
"Known Exploited Vulnerabilities."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_active_findings_within_max_age",
|
||||
"CheckTitle": "Inspector2 has no active findings older than the configured maximum age",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Vulnerabilities/CVE",
|
||||
"Software and Configuration Checks/Patch Management",
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**Amazon Inspector** active findings are evaluated against the configurable `inspector2_active_finding_max_age_days` threshold (192 days by default), using the time since each finding was first observed (`firstObservedAt`). Suppressed and closed findings are not active and are not evaluated.\n\nThe result is reported per Region where Inspector is enabled.",
|
||||
"Risk": "Findings left open for months show that **vulnerability response** is not keeping up. Long-lived vulnerabilities give attackers time to discover and exploit them, and a backlog that is neither fixed nor formally accepted hides real risk from decision makers.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/findings-understanding.html",
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/findings-managing-supression-rules.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the Amazon Inspector console, open Findings and filter by Finding status = Active\n2. Remediate the findings first observed longest ago\n3. For vulnerabilities you formally accept, choose Suppression rules in the navigation pane and create a rule so they stop counting as active",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Remediate findings within your vulnerability response timeframes. Vulnerabilities you decide not to fix should be formally **accepted** and suppressed with a documented justification instead of staying active indefinitely.",
|
||||
"Url": "https://hub.prowler.com/check/inspector2_active_findings_within_max_age"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"vulnerabilities"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"inspector2_active_findings_exist"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.inspector2.inspector2_client import (
|
||||
inspector2_client,
|
||||
)
|
||||
|
||||
|
||||
class inspector2_active_findings_within_max_age(Check):
|
||||
"""Ensure no Inspector2 finding stays active longer than the configured days."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report, per Region, whether any active finding is older than the allowed days."""
|
||||
findings = []
|
||||
max_age_days = inspector2_client.audit_config.get(
|
||||
"inspector2_active_finding_max_age_days", 192
|
||||
)
|
||||
max_age = timedelta(days=max_age_days)
|
||||
now = datetime.now(timezone.utc)
|
||||
for inspector in inspector2_client.inspectors:
|
||||
if inspector.status != "ENABLED":
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=inspector)
|
||||
if inspector.findings is None:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 findings could not be retrieved in region {inspector.region}; "
|
||||
"verify the inspector2:ListFindings permission."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
stale_ages = [
|
||||
now - finding.first_observed_at
|
||||
for finding in inspector.findings
|
||||
if finding.first_observed_at
|
||||
and now - finding.first_observed_at > max_age
|
||||
]
|
||||
if stale_ages:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has {len(stale_ages)} active findings in region {inspector.region} "
|
||||
f"first observed more than {max_age_days} days ago, the oldest {max(stale_ages).days} days ago."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has no active findings in region {inspector.region} "
|
||||
f"first observed more than {max_age_days} days ago."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_coverage_recently_scanned",
|
||||
"CheckTitle": "Inspector2 covered resource was scanned within the configured number of days",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Vulnerabilities/CVE",
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**Amazon Inspector** coverage is evaluated for every actively monitored resource. The time since the resource was last scanned (`lastScannedAt`) is compared with the configurable `inspector2_max_days_since_last_scan` threshold (3 days by default).\n\nResources still pending their first scan are not evaluated.",
|
||||
"Risk": "Stale scans leave **newly published CVEs** and configuration **drift** undetected. A resource that has not been rescanned for weeks can keep running exploitable packages long after a fix is available.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/assessing-coverage.html",
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/scanning-ecr.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the Amazon Inspector console, choose Account management and review the Last scanned at value in the Instances, Container images and Lambda functions tabs\n2. For EC2 instances, confirm the SSM Agent is healthy or, under General settings > EC2 scanning settings, set the scan mode to hybrid\n3. For ECR images, increase the Amazon ECR re-scan duration in the Amazon Inspector settings\n4. Confirm the resources are rescanned",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Keep continuous scanning healthy: use **hybrid** EC2 scanning so instances without a working SSM agent are still scanned, set a long ECR **rescan duration** for images in use, and investigate every resource whose last scan is older than the allowed window.",
|
||||
"Url": "https://hub.prowler.com/check/inspector2_coverage_recently_scanned"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"vulnerabilities"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_coverage_scan_status_active"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.inspector2.inspector2_client import (
|
||||
inspector2_client,
|
||||
)
|
||||
|
||||
PENDING_SCAN_REASONS = {
|
||||
"PENDING_INITIAL_SCAN",
|
||||
"PENDING_REVIVAL_SCAN",
|
||||
"SCAN_IN_PROGRESS",
|
||||
}
|
||||
|
||||
|
||||
class inspector2_coverage_recently_scanned(Check):
|
||||
"""Ensure Inspector2 scanned every actively covered resource within the configured days."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report whether each actively covered resource was scanned within the allowed days."""
|
||||
findings = []
|
||||
max_days = inspector2_client.audit_config.get(
|
||||
"inspector2_max_days_since_last_scan", 3
|
||||
)
|
||||
max_elapsed = timedelta(days=max_days)
|
||||
now = datetime.now(timezone.utc)
|
||||
for inspector in inspector2_client.inspectors:
|
||||
if inspector.status != "ENABLED":
|
||||
continue
|
||||
if inspector.coverage is None:
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=inspector)
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 coverage could not be retrieved in region {inspector.region}; "
|
||||
"verify the inspector2:ListCoverage permission."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
for resource in inspector.coverage:
|
||||
if resource.scan_status_code != "ACTIVE":
|
||||
continue
|
||||
if (
|
||||
resource.last_scanned_at is None
|
||||
and resource.scan_status_reason in PENDING_SCAN_REASONS
|
||||
):
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=resource)
|
||||
if resource.last_scanned_at is None:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{resource.resource_type} {resource.id} has no recorded Inspector2 scan."
|
||||
elif now - resource.last_scanned_at > max_elapsed:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{resource.resource_type} {resource.id} was last scanned by Inspector2 more than {max_days} days ago."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"{resource.resource_type} {resource.id} was last scanned by Inspector2 within the last {max_days} days."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_coverage_scan_status_active",
|
||||
"CheckTitle": "Inspector2 covered resource is actively scanned",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Vulnerabilities/CVE",
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**Amazon Inspector** coverage is evaluated for every resource it tracks (EC2 instances, ECR images and repositories, Lambda functions). A resource whose scan status is `INACTIVE`, for example because of `UNMANAGED_EC2_INSTANCE`, `NO_INVENTORY`, `UNSUPPORTED_OS` or `ACCESS_DENIED`, is not being scanned for vulnerabilities.\n\nStopped, terminated, tag-excluded and aged-out resources are not evaluated.",
|
||||
"Risk": "Resources that Inspector cannot scan silently fall out of **vulnerability detection**. New CVEs affecting those workloads are never reported, so exploitable software can stay deployed while the account still appears covered.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/assessing-coverage.html",
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/scanning-ec2.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the Amazon Inspector console, choose Account management\n2. Open the Instances, Container images or Lambda functions tab and review the resources that are not actively scanned\n3. Fix the reported cause: register EC2 instances with Systems Manager or set the EC2 scan mode to hybrid, use supported operating systems and runtimes, and grant access to the required encryption keys\n4. Confirm the resource is actively scanned",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Resolve the reason reported in each inactive resource's scan status so Inspector can scan every in-scope workload. Register EC2 instances with **Systems Manager** or enable **hybrid scanning**, keep operating systems and runtimes supported, and treat scanning gaps as vulnerabilities to track.",
|
||||
"Url": "https://hub.prowler.com/check/inspector2_coverage_scan_status_active"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"vulnerabilities"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"inspector2_is_enabled"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.inspector2.inspector2_client import (
|
||||
inspector2_client,
|
||||
)
|
||||
|
||||
NOT_APPLICABLE_SCAN_REASONS = {
|
||||
"EC2_INSTANCE_STOPPED",
|
||||
"EXCLUDED_BY_TAG",
|
||||
"NO_RESOURCES_FOUND",
|
||||
"PENDING_DISABLE",
|
||||
"RESOURCE_TERMINATED",
|
||||
"SCAN_ELIGIBILITY_EXPIRED",
|
||||
}
|
||||
|
||||
|
||||
class inspector2_coverage_scan_status_active(Check):
|
||||
"""Ensure Inspector2 is actively scanning every covered resource."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report whether Inspector2 is actively scanning each covered resource."""
|
||||
findings = []
|
||||
for inspector in inspector2_client.inspectors:
|
||||
if inspector.status != "ENABLED":
|
||||
continue
|
||||
if inspector.coverage is None:
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=inspector)
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 coverage could not be retrieved in region {inspector.region}; "
|
||||
"verify the inspector2:ListCoverage permission."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
for resource in inspector.coverage:
|
||||
if resource.scan_status_reason in NOT_APPLICABLE_SCAN_REASONS:
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=resource)
|
||||
if resource.scan_status_code == "ACTIVE":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Inspector2 is actively scanning {resource.resource_type} {resource.id}."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
reason = resource.scan_status_reason or "no reason reported"
|
||||
report.status_extended = f"Inspector2 is not scanning {resource.resource_type} {resource.id}: {reason}."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -1,16 +1,33 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
|
||||
from prowler.providers.aws.lib.service.service import AWSService
|
||||
|
||||
FINDING_DETAILS_BATCH_SIZE = 10
|
||||
|
||||
|
||||
class Inspector2(AWSService):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.inspectors = []
|
||||
self.known_exploited_vulnerabilities = {}
|
||||
self.vulnerability_lookup_failed = set()
|
||||
self.__threading_call__(self._batch_get_account_status)
|
||||
self.__threading_call__(self._list_active_findings, self.inspectors)
|
||||
enabled_inspectors = [
|
||||
inspector for inspector in self.inspectors if inspector.status == "ENABLED"
|
||||
]
|
||||
self.__threading_call__(self._list_findings, enabled_inspectors)
|
||||
self.__threading_call__(self._list_coverage, enabled_inspectors)
|
||||
self.__threading_call__(
|
||||
self._batch_get_finding_details,
|
||||
self._get_finding_detail_batches(enabled_inspectors),
|
||||
)
|
||||
|
||||
def _batch_get_account_status(self, regional_client):
|
||||
# We use this function to check if inspector2 is enabled
|
||||
@@ -59,6 +76,188 @@ class Inspector2(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_findings(self, inspector):
|
||||
"""Store the active findings of the audited account for an enabled Region."""
|
||||
logger.info("Inspector2 - Listing active findings details...")
|
||||
try:
|
||||
paginator = self.regional_clients[inspector.region].get_paginator(
|
||||
"list_findings"
|
||||
)
|
||||
findings = []
|
||||
for page in paginator.paginate(
|
||||
filterCriteria={
|
||||
"awsAccountId": [
|
||||
{"comparison": "EQUALS", "value": self.audited_account},
|
||||
],
|
||||
"findingStatus": [{"comparison": "EQUALS", "value": "ACTIVE"}],
|
||||
},
|
||||
PaginationConfig={"PageSize": 100},
|
||||
):
|
||||
for finding in page.get("findings", []):
|
||||
findings.append(
|
||||
Finding(
|
||||
arn=finding.get("findingArn", ""),
|
||||
type=finding.get("type", ""),
|
||||
severity=finding.get("severity", ""),
|
||||
first_observed_at=finding.get("firstObservedAt"),
|
||||
vulnerability_id=finding.get(
|
||||
"packageVulnerabilityDetails", {}
|
||||
).get("vulnerabilityId"),
|
||||
resource_ids=[
|
||||
resource["id"]
|
||||
for resource in finding.get("resources", [])
|
||||
if resource.get("id")
|
||||
],
|
||||
)
|
||||
)
|
||||
inspector.findings = findings
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{inspector.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_coverage(self, inspector):
|
||||
"""Store the resources Inspector2 covers in an enabled Region, respecting audit resources."""
|
||||
logger.info("Inspector2 - Listing coverage...")
|
||||
try:
|
||||
paginator = self.regional_clients[inspector.region].get_paginator(
|
||||
"list_coverage"
|
||||
)
|
||||
coverage = []
|
||||
for page in paginator.paginate(
|
||||
filterCriteria={
|
||||
"accountId": [
|
||||
{"comparison": "EQUALS", "value": self.audited_account},
|
||||
],
|
||||
},
|
||||
PaginationConfig={"PageSize": 200},
|
||||
):
|
||||
for covered_resource in page.get("coveredResources", []):
|
||||
resource_id = covered_resource.get("resourceId", "")
|
||||
resource_type = covered_resource.get("resourceType", "")
|
||||
scan_status = covered_resource.get("scanStatus", {})
|
||||
arn = self._get_covered_resource_arn(
|
||||
resource_type, resource_id, inspector.region
|
||||
)
|
||||
if self.audit_resources and not is_resource_filtered(
|
||||
arn, self.audit_resources
|
||||
):
|
||||
continue
|
||||
coverage.append(
|
||||
CoveredResource(
|
||||
id=resource_id,
|
||||
arn=arn,
|
||||
region=inspector.region,
|
||||
resource_type=resource_type,
|
||||
scan_type=covered_resource.get("scanType", ""),
|
||||
scan_status_code=scan_status.get("statusCode", ""),
|
||||
scan_status_reason=scan_status.get("reason", ""),
|
||||
last_scanned_at=covered_resource.get("lastScannedAt"),
|
||||
)
|
||||
)
|
||||
inspector.coverage = coverage
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{inspector.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_covered_resource_arn(self, resource_type, resource_id, region):
|
||||
"""Return the ARN of a covered resource, building it for EC2 instance IDs."""
|
||||
if resource_type == "AWS_EC2_INSTANCE" and not resource_id.startswith("arn:"):
|
||||
return f"arn:{self.audited_partition}:ec2:{region}:{self.audited_account}:instance/{resource_id}"
|
||||
return resource_id
|
||||
|
||||
@staticmethod
|
||||
def _get_finding_detail_batches(inspectors):
|
||||
"""Group one active finding per CVE into finding details batches per Region."""
|
||||
representatives = {}
|
||||
for inspector in inspectors:
|
||||
for finding in inspector.findings or []:
|
||||
if finding.vulnerability_id and finding.vulnerability_id.startswith(
|
||||
"CVE-"
|
||||
):
|
||||
representatives.setdefault(
|
||||
finding.vulnerability_id, (inspector.region, finding.arn)
|
||||
)
|
||||
findings_by_region = {}
|
||||
for vulnerability_id, (region, finding_arn) in representatives.items():
|
||||
findings_by_region.setdefault(region, []).append(
|
||||
(finding_arn, vulnerability_id)
|
||||
)
|
||||
return [
|
||||
(region, findings[index : index + FINDING_DETAILS_BATCH_SIZE])
|
||||
for region, findings in findings_by_region.items()
|
||||
for index in range(0, len(findings), FINDING_DETAILS_BATCH_SIZE)
|
||||
]
|
||||
|
||||
def _batch_get_finding_details(self, batch):
|
||||
"""Record the CISA KEV data of the CVEs in a batch, flagging failed lookups."""
|
||||
region, findings = batch
|
||||
vulnerability_ids = dict(findings)
|
||||
logger.info("Inspector2 - Getting finding details...")
|
||||
try:
|
||||
response = self.regional_clients[region].batch_get_finding_details(
|
||||
findingArns=list(vulnerability_ids)
|
||||
)
|
||||
for detail in response.get("findingDetails", []):
|
||||
vulnerability_id = vulnerability_ids.get(detail.get("findingArn"))
|
||||
cisa_data = detail.get("cisaData")
|
||||
if vulnerability_id and cisa_data:
|
||||
self.known_exploited_vulnerabilities[vulnerability_id] = (
|
||||
KnownExploitedVulnerability(
|
||||
id=vulnerability_id,
|
||||
date_added=cisa_data.get("dateAdded"),
|
||||
date_due=cisa_data.get("dateDue"),
|
||||
)
|
||||
)
|
||||
for detail_error in response.get("errors", []):
|
||||
# Inspector has no intelligence for the CVE, so it cannot be a KEV
|
||||
if detail_error.get("errorCode") == "FINDING_DETAILS_NOT_FOUND":
|
||||
continue
|
||||
vulnerability_id = vulnerability_ids.get(detail_error.get("findingArn"))
|
||||
if vulnerability_id:
|
||||
self.vulnerability_lookup_failed.add(vulnerability_id)
|
||||
logger.error(
|
||||
f"{region} -- {detail_error.get('errorCode')} getting finding details for {vulnerability_id}: {detail_error.get('errorMessage')}"
|
||||
)
|
||||
except Exception as error:
|
||||
self.vulnerability_lookup_failed.update(vulnerability_ids.values())
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class Finding(BaseModel):
|
||||
"""Active Inspector2 finding."""
|
||||
|
||||
arn: str
|
||||
type: str
|
||||
severity: str
|
||||
first_observed_at: Optional[datetime]
|
||||
vulnerability_id: Optional[str]
|
||||
resource_ids: list[str] = []
|
||||
|
||||
|
||||
class CoveredResource(BaseModel):
|
||||
"""Resource tracked by Inspector2 coverage."""
|
||||
|
||||
id: str
|
||||
arn: str
|
||||
region: str
|
||||
resource_type: str
|
||||
scan_type: str
|
||||
scan_status_code: str
|
||||
scan_status_reason: str
|
||||
last_scanned_at: Optional[datetime]
|
||||
|
||||
|
||||
class KnownExploitedVulnerability(BaseModel):
|
||||
"""CISA Known Exploited Vulnerability data of a CVE."""
|
||||
|
||||
id: str
|
||||
date_added: Optional[datetime]
|
||||
date_due: Optional[datetime]
|
||||
|
||||
|
||||
class Inspector(BaseModel):
|
||||
id: str
|
||||
@@ -70,3 +269,5 @@ class Inspector(BaseModel):
|
||||
lambda_status: str
|
||||
lambda_code_status: str
|
||||
active_findings: bool = None
|
||||
findings: Optional[list[Finding]] = None
|
||||
coverage: Optional[list[CoveredResource]] = None
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
MAX_LISTED_VULNERABILITIES = 10
|
||||
|
||||
|
||||
def summarize_vulnerabilities(vulnerabilities: list[str]) -> str:
|
||||
"""Join vulnerability identifiers, truncating long lists."""
|
||||
listed = ", ".join(vulnerabilities[:MAX_LISTED_VULNERABILITIES])
|
||||
remaining = len(vulnerabilities) - MAX_LISTED_VULNERABILITIES
|
||||
return f"{listed} and {remaining} more" if remaining > 0 else listed
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "transfer_server_fips_security_policy_enabled",
|
||||
"CheckTitle": "AWS Transfer Family server uses a FIPS security policy",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "transfer",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "low",
|
||||
"ResourceType": "AwsTransferServer",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**AWS Transfer Family servers** (SFTP, FTPS, AS2) are assessed for use of a **FIPS** security policy (`TransferSecurityPolicy-FIPS-*`, flagged `Fips: true` by AWS), which limits file-transfer sessions to the FIPS-enabled set of SSH and TLS algorithms.",
|
||||
"Risk": "Servers without a FIPS security policy can negotiate algorithms outside the FIPS-enabled set, which does not meet requirements to protect federal or regulated files and credentials with **NIST CMVP validated cryptography**.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/transfer/latest/userguide/security-policies.html",
|
||||
"https://aws.amazon.com/compliance/fips/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws transfer update-server --server-id <server_id> --security-policy-name TransferSecurityPolicy-FIPS-2025-03",
|
||||
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::Transfer::Server\n Properties:\n Protocols:\n - SFTP\n SecurityPolicyName: TransferSecurityPolicy-FIPS-2025-03 # FIX: FIPS security policy\n```",
|
||||
"Other": "1. In the AWS Console, go to AWS Transfer Family > Servers\n2. Select the server and choose Edit on the Additional details panel\n3. Set Cryptographic algorithm options (Security policy) to a FIPS policy such as TransferSecurityPolicy-FIPS-2025-03\n4. Save the changes",
|
||||
"Terraform": "```hcl\nresource \"aws_transfer_server\" \"<example_resource_name>\" {\n protocols = [\"SFTP\"]\n security_policy_name = \"TransferSecurityPolicy-FIPS-2025-03\" # FIX: FIPS security policy\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use a **FIPS** security policy, such as `TransferSecurityPolicy-FIPS-2025-03`, on every Transfer Family server that exchanges federal or regulated data.",
|
||||
"Url": "https://hub.prowler.com/check/transfer_server_fips_security_policy_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"transfer_server_in_transit_encryption_enabled",
|
||||
"transfer_server_pqc_ssh_kex_enabled"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.transfer.transfer_client import transfer_client
|
||||
|
||||
|
||||
class transfer_server_fips_security_policy_enabled(Check):
|
||||
"""Ensure every AWS Transfer Family server uses a FIPS security policy."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report whether each Transfer Family server uses a FIPS security policy."""
|
||||
findings = []
|
||||
unretrieved_servers = []
|
||||
for server in transfer_client.servers.values():
|
||||
policy = server.security_policy_name
|
||||
if not policy:
|
||||
unretrieved_servers.append(server.id)
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=server)
|
||||
if "FIPS" in policy.split("-"):
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Transfer Server {server.id} uses FIPS security policy {policy}."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Transfer Server {server.id} uses security policy {policy}, which is not a FIPS security policy."
|
||||
findings.append(report)
|
||||
if unretrieved_servers:
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource={})
|
||||
report.resource_id = transfer_client.audited_account
|
||||
report.resource_arn = transfer_client.audited_account_arn
|
||||
report.region = transfer_client.region
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
"Transfer Server security policies could not be retrieved for "
|
||||
f"{', '.join(unretrieved_servers)}; verify the transfer:DescribeServer permission."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -138,6 +138,8 @@ config_aws = {
|
||||
"organizations_enabled_regions": [],
|
||||
"organizations_trusted_delegated_administrators": [],
|
||||
"ecr_repository_vulnerability_minimum_severity": "MEDIUM",
|
||||
"inspector2_max_days_since_last_scan": 3,
|
||||
"inspector2_active_finding_max_age_days": 192,
|
||||
"verify_premium_support_plans": True,
|
||||
"threat_detection_privilege_escalation_threshold": 0.2,
|
||||
"threat_detection_privilege_escalation_minutes": 1440,
|
||||
|
||||
@@ -139,6 +139,14 @@ aws:
|
||||
# MEDIUM
|
||||
ecr_repository_vulnerability_minimum_severity: "MEDIUM"
|
||||
|
||||
# AWS Inspector2
|
||||
# aws.inspector2_coverage_recently_scanned
|
||||
# Maximum days since Inspector2 last scanned an actively covered resource
|
||||
inspector2_max_days_since_last_scan: 3
|
||||
# aws.inspector2_active_findings_within_max_age
|
||||
# Maximum days an Inspector2 finding can stay active since it was first observed
|
||||
inspector2_active_finding_max_age_days: 192
|
||||
|
||||
# AWS Trusted Advisor
|
||||
# aws.trustedadvisor_premium_support_plan_subscribed
|
||||
verify_premium_support_plans: True
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
from unittest import mock
|
||||
|
||||
from boto3 import client, resource
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_REGION_EU_WEST_1,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
CHECK_MODULE = "prowler.providers.aws.services.elbv2.elbv2_listener_fips_tls_enabled.elbv2_listener_fips_tls_enabled"
|
||||
FIPS_POLICY = "ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04"
|
||||
NON_FIPS_POLICY = "ELBSecurityPolicy-TLS13-1-2-2021-06"
|
||||
|
||||
|
||||
def create_application_load_balancer():
|
||||
conn = client("elbv2", region_name=AWS_REGION_EU_WEST_1)
|
||||
ec2 = resource("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
security_group = ec2.create_security_group(
|
||||
GroupName="a-security-group", Description="First One"
|
||||
)
|
||||
vpc = ec2.create_vpc(CidrBlock="172.28.7.0/24", InstanceTenancy="default")
|
||||
subnet1 = ec2.create_subnet(
|
||||
VpcId=vpc.id,
|
||||
CidrBlock="172.28.7.192/26",
|
||||
AvailabilityZone=f"{AWS_REGION_EU_WEST_1}a",
|
||||
)
|
||||
subnet2 = ec2.create_subnet(
|
||||
VpcId=vpc.id,
|
||||
CidrBlock="172.28.7.0/26",
|
||||
AvailabilityZone=f"{AWS_REGION_EU_WEST_1}b",
|
||||
)
|
||||
lb = conn.create_load_balancer(
|
||||
Name="my-lb",
|
||||
Subnets=[subnet1.id, subnet2.id],
|
||||
SecurityGroups=[security_group.id],
|
||||
Scheme="internal",
|
||||
Type="application",
|
||||
)["LoadBalancers"][0]
|
||||
target_group_arn = conn.create_target_group(
|
||||
Name="a-target", Protocol="HTTP", Port=8080, VpcId=vpc.id
|
||||
)["TargetGroups"][0]["TargetGroupArn"]
|
||||
return conn, lb, target_group_arn
|
||||
|
||||
|
||||
def create_listener(conn, lb, target_group_arn, protocol, port, ssl_policy=None):
|
||||
listener_args = {
|
||||
"LoadBalancerArn": lb["LoadBalancerArn"],
|
||||
"Protocol": protocol,
|
||||
"Port": port,
|
||||
"DefaultActions": [{"Type": "forward", "TargetGroupArn": target_group_arn}],
|
||||
}
|
||||
if ssl_policy:
|
||||
listener_args["SslPolicy"] = ssl_policy
|
||||
return conn.create_listener(**listener_args)["Listeners"][0]
|
||||
|
||||
|
||||
def execute_check(service=None):
|
||||
from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2
|
||||
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1],
|
||||
create_default_organization=False,
|
||||
)
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(f"{CHECK_MODULE}.elbv2_client", new=service or ELBv2(aws_provider)),
|
||||
):
|
||||
from prowler.providers.aws.services.elbv2.elbv2_listener_fips_tls_enabled.elbv2_listener_fips_tls_enabled import (
|
||||
elbv2_listener_fips_tls_enabled,
|
||||
)
|
||||
|
||||
return elbv2_listener_fips_tls_enabled().execute()
|
||||
|
||||
|
||||
class Test_elbv2_listener_fips_tls_enabled:
|
||||
@mock_aws
|
||||
def test_no_load_balancers(self):
|
||||
assert execute_check() == []
|
||||
|
||||
@mock_aws
|
||||
def test_http_listener_only(self):
|
||||
conn, lb, target_group_arn = create_application_load_balancer()
|
||||
create_listener(conn, lb, target_group_arn, "HTTP", 80)
|
||||
|
||||
result = execute_check()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == "ELBv2 my-lb has no HTTPS/TLS listeners."
|
||||
|
||||
@mock_aws
|
||||
def test_fips_policy(self):
|
||||
conn, lb, target_group_arn = create_application_load_balancer()
|
||||
create_listener(conn, lb, target_group_arn, "HTTPS", 443, FIPS_POLICY)
|
||||
|
||||
result = execute_check()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "ELBv2 my-lb has all HTTPS/TLS listeners using a FIPS TLS security policy."
|
||||
)
|
||||
assert result[0].resource_id == "my-lb"
|
||||
assert result[0].resource_arn == lb["LoadBalancerArn"]
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
|
||||
@mock_aws
|
||||
def test_non_fips_policy(self):
|
||||
conn, lb, target_group_arn = create_application_load_balancer()
|
||||
listener = create_listener(
|
||||
conn, lb, target_group_arn, "HTTPS", 443, NON_FIPS_POLICY
|
||||
)
|
||||
|
||||
result = execute_check()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"ELBv2 my-lb has HTTPS/TLS listeners without a FIPS TLS security policy: HTTPS:443 ({listener['ListenerArn']}) uses {NON_FIPS_POLICY}."
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_mixed_listeners(self):
|
||||
conn, lb, target_group_arn = create_application_load_balancer()
|
||||
create_listener(conn, lb, target_group_arn, "HTTPS", 443, FIPS_POLICY)
|
||||
create_listener(conn, lb, target_group_arn, "HTTPS", 8443, NON_FIPS_POLICY)
|
||||
|
||||
result = execute_check()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert NON_FIPS_POLICY in result[0].status_extended
|
||||
assert FIPS_POLICY not in result[0].status_extended
|
||||
|
||||
@mock_aws
|
||||
def test_listener_discovery_failed(self):
|
||||
from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2
|
||||
|
||||
conn, lb, target_group_arn = create_application_load_balancer()
|
||||
create_listener(conn, lb, target_group_arn, "HTTPS", 443, NON_FIPS_POLICY)
|
||||
service = ELBv2(
|
||||
set_mocked_aws_provider(
|
||||
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1],
|
||||
create_default_organization=False,
|
||||
)
|
||||
)
|
||||
service.loadbalancersv2[lb["LoadBalancerArn"]].listener_discovery_failed = True
|
||||
|
||||
assert execute_check(service) == []
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.aws.services.inspector2.inspector2_service import (
|
||||
Finding,
|
||||
Inspector,
|
||||
KnownExploitedVulnerability,
|
||||
)
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_EU_WEST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
INSPECTOR_ARN = (
|
||||
f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2"
|
||||
)
|
||||
FINDING_ARN = f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:finding/0e436649379db5f327e3cf5bb4421d76"
|
||||
KEV_ID = "CVE-2024-3400"
|
||||
CHECK_MODULE = "prowler.providers.aws.services.inspector2.inspector2_active_findings_kev_within_due_date.inspector2_active_findings_kev_within_due_date"
|
||||
|
||||
|
||||
def build_inspector(findings=None, status="ENABLED"):
|
||||
return Inspector(
|
||||
id="Inspector2",
|
||||
arn=INSPECTOR_ARN,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
status=status,
|
||||
ec2_status="ENABLED",
|
||||
ecr_status="ENABLED",
|
||||
lambda_status="ENABLED",
|
||||
lambda_code_status="ENABLED",
|
||||
findings=findings,
|
||||
)
|
||||
|
||||
|
||||
def build_finding(vulnerability_id=KEV_ID):
|
||||
return Finding(
|
||||
arn=FINDING_ARN,
|
||||
type="PACKAGE_VULNERABILITY",
|
||||
severity="CRITICAL",
|
||||
first_observed_at=datetime.now(timezone.utc),
|
||||
vulnerability_id=vulnerability_id,
|
||||
resource_ids=["i-0123456789abcdef0"],
|
||||
)
|
||||
|
||||
|
||||
def build_kev(date_due):
|
||||
return KnownExploitedVulnerability(
|
||||
id=KEV_ID,
|
||||
date_added=datetime(2024, 4, 12, tzinfo=timezone.utc),
|
||||
date_due=date_due,
|
||||
)
|
||||
|
||||
|
||||
def execute_check(inspectors, known_exploited=None, lookup_failed=None):
|
||||
inspector2_client = mock.MagicMock()
|
||||
inspector2_client.inspectors = inspectors
|
||||
inspector2_client.known_exploited_vulnerabilities = known_exploited or {}
|
||||
inspector2_client.vulnerability_lookup_failed = lookup_failed or set()
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_EU_WEST_1]),
|
||||
),
|
||||
mock.patch(f"{CHECK_MODULE}.inspector2_client", new=inspector2_client),
|
||||
):
|
||||
from prowler.providers.aws.services.inspector2.inspector2_active_findings_kev_within_due_date.inspector2_active_findings_kev_within_due_date import (
|
||||
inspector2_active_findings_kev_within_due_date,
|
||||
)
|
||||
|
||||
return inspector2_active_findings_kev_within_due_date().execute()
|
||||
|
||||
|
||||
class Test_inspector2_active_findings_kev_within_due_date:
|
||||
def test_no_resources(self):
|
||||
assert execute_check([]) == []
|
||||
|
||||
def test_inspector_disabled(self):
|
||||
assert execute_check([build_inspector(findings=[], status="DISABLED")]) == []
|
||||
|
||||
def test_kev_past_due_date(self):
|
||||
result = execute_check(
|
||||
[build_inspector(findings=[build_finding()])],
|
||||
known_exploited={
|
||||
KEV_ID: build_kev(datetime(2024, 4, 19, tzinfo=timezone.utc))
|
||||
},
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 has active findings in region {AWS_REGION_EU_WEST_1} for CISA Known Exploited Vulnerabilities past their remediation due date: {KEV_ID} (due 2024-04-19)."
|
||||
)
|
||||
assert result[0].resource_id == "Inspector2"
|
||||
assert result[0].resource_arn == INSPECTOR_ARN
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
|
||||
def test_kev_within_due_date(self):
|
||||
result = execute_check(
|
||||
[build_inspector(findings=[build_finding()])],
|
||||
known_exploited={
|
||||
KEV_ID: build_kev(datetime.now(timezone.utc) + timedelta(days=7))
|
||||
},
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 has no active findings in region {AWS_REGION_EU_WEST_1} for CISA Known Exploited Vulnerabilities past their remediation due date."
|
||||
)
|
||||
|
||||
def test_no_kev_findings(self):
|
||||
result = execute_check(
|
||||
[build_inspector(findings=[build_finding("CVE-2022-40897")])]
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_kev_status_not_verified(self):
|
||||
result = execute_check(
|
||||
[build_inspector(findings=[build_finding()])],
|
||||
lookup_failed={KEV_ID},
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
|
||||
def test_findings_not_retrieved(self):
|
||||
result = execute_check([build_inspector(findings=None)])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.aws.services.inspector2.inspector2_service import (
|
||||
Finding,
|
||||
Inspector,
|
||||
KnownExploitedVulnerability,
|
||||
)
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_EU_WEST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
INSPECTOR_ARN = (
|
||||
f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2"
|
||||
)
|
||||
FINDING_ARN = f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:finding/0e436649379db5f327e3cf5bb4421d76"
|
||||
KEV_ID = "CVE-2024-3400"
|
||||
CHECK_MODULE = "prowler.providers.aws.services.inspector2.inspector2_active_findings_no_known_exploited_vulnerabilities.inspector2_active_findings_no_known_exploited_vulnerabilities"
|
||||
|
||||
|
||||
def build_inspector(findings=None, status="ENABLED"):
|
||||
return Inspector(
|
||||
id="Inspector2",
|
||||
arn=INSPECTOR_ARN,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
status=status,
|
||||
ec2_status="ENABLED",
|
||||
ecr_status="ENABLED",
|
||||
lambda_status="ENABLED",
|
||||
lambda_code_status="ENABLED",
|
||||
findings=findings,
|
||||
)
|
||||
|
||||
|
||||
def build_finding(vulnerability_id):
|
||||
return Finding(
|
||||
arn=FINDING_ARN,
|
||||
type="PACKAGE_VULNERABILITY",
|
||||
severity="CRITICAL",
|
||||
first_observed_at=datetime.now(timezone.utc),
|
||||
vulnerability_id=vulnerability_id,
|
||||
resource_ids=["i-0123456789abcdef0"],
|
||||
)
|
||||
|
||||
|
||||
def build_kev(vulnerability_id):
|
||||
return KnownExploitedVulnerability(
|
||||
id=vulnerability_id,
|
||||
date_added=datetime(2024, 4, 12, tzinfo=timezone.utc),
|
||||
date_due=datetime(2024, 4, 19, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
def execute_check(inspectors, known_exploited=None, lookup_failed=None):
|
||||
inspector2_client = mock.MagicMock()
|
||||
inspector2_client.inspectors = inspectors
|
||||
inspector2_client.known_exploited_vulnerabilities = known_exploited or {}
|
||||
inspector2_client.vulnerability_lookup_failed = lookup_failed or set()
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_EU_WEST_1]),
|
||||
),
|
||||
mock.patch(f"{CHECK_MODULE}.inspector2_client", new=inspector2_client),
|
||||
):
|
||||
from prowler.providers.aws.services.inspector2.inspector2_active_findings_no_known_exploited_vulnerabilities.inspector2_active_findings_no_known_exploited_vulnerabilities import (
|
||||
inspector2_active_findings_no_known_exploited_vulnerabilities,
|
||||
)
|
||||
|
||||
return inspector2_active_findings_no_known_exploited_vulnerabilities().execute()
|
||||
|
||||
|
||||
class Test_inspector2_active_findings_no_known_exploited_vulnerabilities:
|
||||
def test_no_resources(self):
|
||||
assert execute_check([]) == []
|
||||
|
||||
def test_inspector_disabled(self):
|
||||
assert execute_check([build_inspector(findings=[], status="DISABLED")]) == []
|
||||
|
||||
def test_no_kev_findings(self):
|
||||
result = execute_check(
|
||||
[build_inspector(findings=[build_finding("CVE-2022-40897")])]
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 has no active findings in region {AWS_REGION_EU_WEST_1} for CISA Known Exploited Vulnerabilities."
|
||||
)
|
||||
assert result[0].resource_id == "Inspector2"
|
||||
assert result[0].resource_arn == INSPECTOR_ARN
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
|
||||
def test_kev_finding(self):
|
||||
result = execute_check(
|
||||
[
|
||||
build_inspector(
|
||||
findings=[build_finding(KEV_ID), build_finding("CVE-2022-40897")]
|
||||
)
|
||||
],
|
||||
known_exploited={KEV_ID: build_kev(KEV_ID)},
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 has active findings in region {AWS_REGION_EU_WEST_1} for CISA Known Exploited Vulnerabilities: {KEV_ID}."
|
||||
)
|
||||
|
||||
def test_many_kev_findings_are_truncated(self):
|
||||
vulnerability_ids = [f"CVE-2024-{number:04d}" for number in range(1, 13)]
|
||||
result = execute_check(
|
||||
[
|
||||
build_inspector(
|
||||
findings=[build_finding(vid) for vid in vulnerability_ids]
|
||||
)
|
||||
],
|
||||
known_exploited={vid: build_kev(vid) for vid in vulnerability_ids},
|
||||
)
|
||||
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status_extended.endswith("CVE-2024-0010 and 2 more.")
|
||||
|
||||
def test_kev_status_not_verified(self):
|
||||
result = execute_check(
|
||||
[build_inspector(findings=[build_finding(KEV_ID)])],
|
||||
lookup_failed={KEV_ID},
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 could not verify the CISA Known Exploited Vulnerabilities status of {KEV_ID} in region {AWS_REGION_EU_WEST_1}; verify the inspector2:BatchGetFindingDetails permission."
|
||||
)
|
||||
|
||||
def test_findings_not_retrieved(self):
|
||||
result = execute_check([build_inspector(findings=None)])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 findings could not be retrieved in region {AWS_REGION_EU_WEST_1}; verify the inspector2:ListFindings permission."
|
||||
)
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.aws.services.inspector2.inspector2_service import (
|
||||
Finding,
|
||||
Inspector,
|
||||
)
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_EU_WEST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
INSPECTOR_ARN = (
|
||||
f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2"
|
||||
)
|
||||
FINDING_ARN = f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:finding/0e436649379db5f327e3cf5bb4421d76"
|
||||
CHECK_MODULE = "prowler.providers.aws.services.inspector2.inspector2_active_findings_within_max_age.inspector2_active_findings_within_max_age"
|
||||
|
||||
|
||||
def build_inspector(findings=None, status="ENABLED"):
|
||||
return Inspector(
|
||||
id="Inspector2",
|
||||
arn=INSPECTOR_ARN,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
status=status,
|
||||
ec2_status="ENABLED",
|
||||
ecr_status="ENABLED",
|
||||
lambda_status="ENABLED",
|
||||
lambda_code_status="ENABLED",
|
||||
findings=findings,
|
||||
)
|
||||
|
||||
|
||||
def build_finding(age_days):
|
||||
first_observed_at = (
|
||||
datetime.now(timezone.utc) - timedelta(days=age_days, hours=1)
|
||||
if age_days is not None
|
||||
else None
|
||||
)
|
||||
return Finding(
|
||||
arn=FINDING_ARN,
|
||||
type="PACKAGE_VULNERABILITY",
|
||||
severity="HIGH",
|
||||
first_observed_at=first_observed_at,
|
||||
vulnerability_id="CVE-2022-40897",
|
||||
resource_ids=["i-0123456789abcdef0"],
|
||||
)
|
||||
|
||||
|
||||
def execute_check(inspectors, audit_config=None):
|
||||
inspector2_client = mock.MagicMock()
|
||||
inspector2_client.inspectors = inspectors
|
||||
inspector2_client.audit_config = audit_config or {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_EU_WEST_1]),
|
||||
),
|
||||
mock.patch(f"{CHECK_MODULE}.inspector2_client", new=inspector2_client),
|
||||
):
|
||||
from prowler.providers.aws.services.inspector2.inspector2_active_findings_within_max_age.inspector2_active_findings_within_max_age import (
|
||||
inspector2_active_findings_within_max_age,
|
||||
)
|
||||
|
||||
return inspector2_active_findings_within_max_age().execute()
|
||||
|
||||
|
||||
class Test_inspector2_active_findings_within_max_age:
|
||||
def test_no_resources(self):
|
||||
assert execute_check([]) == []
|
||||
|
||||
def test_inspector_disabled(self):
|
||||
assert execute_check([build_inspector(findings=[], status="DISABLED")]) == []
|
||||
|
||||
def test_no_active_findings(self):
|
||||
result = execute_check([build_inspector(findings=[])])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 has no active findings in region {AWS_REGION_EU_WEST_1} first observed more than 192 days ago."
|
||||
)
|
||||
assert result[0].resource_id == "Inspector2"
|
||||
assert result[0].resource_arn == INSPECTOR_ARN
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
|
||||
def test_recent_findings(self):
|
||||
result = execute_check([build_inspector(findings=[build_finding(30)])])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_stale_findings(self):
|
||||
result = execute_check(
|
||||
[
|
||||
build_inspector(
|
||||
findings=[
|
||||
build_finding(30),
|
||||
build_finding(200),
|
||||
build_finding(400),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 has 2 active findings in region {AWS_REGION_EU_WEST_1} first observed more than 192 days ago, the oldest 400 days ago."
|
||||
)
|
||||
|
||||
def test_finding_just_over_max_age(self):
|
||||
result = execute_check([build_inspector(findings=[build_finding(192)])])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_custom_max_age(self):
|
||||
result = execute_check(
|
||||
[build_inspector(findings=[build_finding(30)])],
|
||||
audit_config={"inspector2_active_finding_max_age_days": 14},
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_finding_without_first_observed_date_is_ignored(self):
|
||||
result = execute_check([build_inspector(findings=[build_finding(None)])])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_findings_not_retrieved(self):
|
||||
result = execute_check([build_inspector(findings=None)])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.aws.services.inspector2.inspector2_service import (
|
||||
CoveredResource,
|
||||
Inspector,
|
||||
)
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_EU_WEST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
INSPECTOR_ARN = (
|
||||
f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2"
|
||||
)
|
||||
INSTANCE_ID = "i-0123456789abcdef0"
|
||||
INSTANCE_ARN = (
|
||||
f"arn:aws:ec2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:instance/{INSTANCE_ID}"
|
||||
)
|
||||
CHECK_MODULE = "prowler.providers.aws.services.inspector2.inspector2_coverage_recently_scanned.inspector2_coverage_recently_scanned"
|
||||
|
||||
|
||||
def build_inspector(coverage=None, status="ENABLED"):
|
||||
return Inspector(
|
||||
id="Inspector2",
|
||||
arn=INSPECTOR_ARN,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
status=status,
|
||||
ec2_status="ENABLED",
|
||||
ecr_status="ENABLED",
|
||||
lambda_status="ENABLED",
|
||||
lambda_code_status="ENABLED",
|
||||
coverage=coverage,
|
||||
)
|
||||
|
||||
|
||||
def build_instance(
|
||||
days_since_scan=None, scan_status_code="ACTIVE", scan_status_reason="SUCCESSFUL"
|
||||
):
|
||||
last_scanned_at = (
|
||||
datetime.now(timezone.utc) - timedelta(days=days_since_scan, hours=1)
|
||||
if days_since_scan is not None
|
||||
else None
|
||||
)
|
||||
return CoveredResource(
|
||||
id=INSTANCE_ID,
|
||||
arn=INSTANCE_ARN,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
resource_type="AWS_EC2_INSTANCE",
|
||||
scan_type="PACKAGE",
|
||||
scan_status_code=scan_status_code,
|
||||
scan_status_reason=scan_status_reason,
|
||||
last_scanned_at=last_scanned_at,
|
||||
)
|
||||
|
||||
|
||||
def execute_check(inspectors, audit_config=None):
|
||||
inspector2_client = mock.MagicMock()
|
||||
inspector2_client.inspectors = inspectors
|
||||
inspector2_client.audit_config = audit_config or {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_EU_WEST_1]),
|
||||
),
|
||||
mock.patch(f"{CHECK_MODULE}.inspector2_client", new=inspector2_client),
|
||||
):
|
||||
from prowler.providers.aws.services.inspector2.inspector2_coverage_recently_scanned.inspector2_coverage_recently_scanned import (
|
||||
inspector2_coverage_recently_scanned,
|
||||
)
|
||||
|
||||
return inspector2_coverage_recently_scanned().execute()
|
||||
|
||||
|
||||
class Test_inspector2_coverage_recently_scanned:
|
||||
def test_no_resources(self):
|
||||
assert execute_check([]) == []
|
||||
|
||||
def test_inspector_disabled(self):
|
||||
assert execute_check([build_inspector(status="DISABLED", coverage=[])]) == []
|
||||
|
||||
def test_recently_scanned_resource(self):
|
||||
result = execute_check([build_inspector(coverage=[build_instance(1)])])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"AWS_EC2_INSTANCE {INSTANCE_ID} was last scanned by Inspector2 within the last 3 days."
|
||||
)
|
||||
assert result[0].resource_id == INSTANCE_ID
|
||||
assert result[0].resource_arn == INSTANCE_ARN
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
|
||||
def test_stale_resource(self):
|
||||
result = execute_check([build_inspector(coverage=[build_instance(10)])])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"AWS_EC2_INSTANCE {INSTANCE_ID} was last scanned by Inspector2 more than 3 days ago."
|
||||
)
|
||||
|
||||
def test_resource_scanned_just_over_max_days(self):
|
||||
result = execute_check([build_inspector(coverage=[build_instance(3)])])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_custom_max_days(self):
|
||||
result = execute_check(
|
||||
[build_inspector(coverage=[build_instance(10)])],
|
||||
audit_config={"inspector2_max_days_since_last_scan": 14},
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_resource_without_recorded_scan(self):
|
||||
result = execute_check([build_inspector(coverage=[build_instance(None)])])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"AWS_EC2_INSTANCE {INSTANCE_ID} has no recorded Inspector2 scan."
|
||||
)
|
||||
|
||||
def test_pending_initial_scan_is_skipped(self):
|
||||
assert (
|
||||
execute_check(
|
||||
[
|
||||
build_inspector(
|
||||
coverage=[
|
||||
build_instance(
|
||||
None, scan_status_reason="PENDING_INITIAL_SCAN"
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
def test_inactive_resource_is_skipped(self):
|
||||
assert (
|
||||
execute_check(
|
||||
[
|
||||
build_inspector(
|
||||
coverage=[
|
||||
build_instance(
|
||||
10,
|
||||
scan_status_code="INACTIVE",
|
||||
scan_status_reason="NO_INVENTORY",
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
def test_coverage_not_retrieved(self):
|
||||
result = execute_check([build_inspector(coverage=None)])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert result[0].resource_arn == INSPECTOR_ARN
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.aws.services.inspector2.inspector2_service import (
|
||||
CoveredResource,
|
||||
Inspector,
|
||||
)
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_EU_WEST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
INSPECTOR_ARN = (
|
||||
f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2"
|
||||
)
|
||||
INSTANCE_ID = "i-0123456789abcdef0"
|
||||
INSTANCE_ARN = (
|
||||
f"arn:aws:ec2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:instance/{INSTANCE_ID}"
|
||||
)
|
||||
CHECK_MODULE = "prowler.providers.aws.services.inspector2.inspector2_coverage_scan_status_active.inspector2_coverage_scan_status_active"
|
||||
|
||||
|
||||
def build_inspector(status="ENABLED", coverage=None):
|
||||
return Inspector(
|
||||
id="Inspector2",
|
||||
arn=INSPECTOR_ARN,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
status=status,
|
||||
ec2_status="ENABLED",
|
||||
ecr_status="ENABLED",
|
||||
lambda_status="ENABLED",
|
||||
lambda_code_status="ENABLED",
|
||||
coverage=coverage,
|
||||
)
|
||||
|
||||
|
||||
def build_instance(scan_status_code, scan_status_reason):
|
||||
return CoveredResource(
|
||||
id=INSTANCE_ID,
|
||||
arn=INSTANCE_ARN,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
resource_type="AWS_EC2_INSTANCE",
|
||||
scan_type="PACKAGE",
|
||||
scan_status_code=scan_status_code,
|
||||
scan_status_reason=scan_status_reason,
|
||||
last_scanned_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
def execute_check(inspectors):
|
||||
inspector2_client = mock.MagicMock()
|
||||
inspector2_client.inspectors = inspectors
|
||||
inspector2_client.audit_config = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_EU_WEST_1]),
|
||||
),
|
||||
mock.patch(f"{CHECK_MODULE}.inspector2_client", new=inspector2_client),
|
||||
):
|
||||
from prowler.providers.aws.services.inspector2.inspector2_coverage_scan_status_active.inspector2_coverage_scan_status_active import (
|
||||
inspector2_coverage_scan_status_active,
|
||||
)
|
||||
|
||||
return inspector2_coverage_scan_status_active().execute()
|
||||
|
||||
|
||||
class Test_inspector2_coverage_scan_status_active:
|
||||
def test_no_resources(self):
|
||||
assert execute_check([]) == []
|
||||
|
||||
def test_inspector_disabled(self):
|
||||
assert execute_check([build_inspector(status="DISABLED", coverage=[])]) == []
|
||||
|
||||
def test_no_covered_resources(self):
|
||||
assert execute_check([build_inspector(coverage=[])]) == []
|
||||
|
||||
def test_active_resource(self):
|
||||
result = execute_check(
|
||||
[build_inspector(coverage=[build_instance("ACTIVE", "SUCCESSFUL")])]
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 is actively scanning AWS_EC2_INSTANCE {INSTANCE_ID}."
|
||||
)
|
||||
assert result[0].resource_id == INSTANCE_ID
|
||||
assert result[0].resource_arn == INSTANCE_ARN
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
|
||||
def test_inactive_resource(self):
|
||||
result = execute_check(
|
||||
[
|
||||
build_inspector(
|
||||
coverage=[build_instance("INACTIVE", "UNMANAGED_EC2_INSTANCE")]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 is not scanning AWS_EC2_INSTANCE {INSTANCE_ID}: UNMANAGED_EC2_INSTANCE."
|
||||
)
|
||||
assert result[0].resource_id == INSTANCE_ID
|
||||
|
||||
def test_not_applicable_resource_is_skipped(self):
|
||||
assert (
|
||||
execute_check(
|
||||
[
|
||||
build_inspector(
|
||||
coverage=[build_instance("INACTIVE", "EC2_INSTANCE_STOPPED")]
|
||||
)
|
||||
]
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
def test_coverage_not_retrieved(self):
|
||||
result = execute_check([build_inspector(coverage=None)])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Inspector2 coverage could not be retrieved in region {AWS_REGION_EU_WEST_1}; verify the inspector2:ListCoverage permission."
|
||||
)
|
||||
assert result[0].resource_arn == INSPECTOR_ARN
|
||||
@@ -1,18 +1,30 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
import botocore
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from prowler.providers.aws.services.inspector2.inspector2_service import Inspector2
|
||||
from prowler.providers.aws.services.inspector2.inspector2_service import (
|
||||
Finding,
|
||||
Inspector,
|
||||
Inspector2,
|
||||
)
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_EU_WEST_1,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
FINDING_ARN = (
|
||||
"arn:aws:inspector2:us-east-1:123456789012:finding/0e436649379db5f327e3cf5bb4421d76"
|
||||
)
|
||||
VULNERABILITY_ID = "CVE-2022-40897"
|
||||
INSTANCE_ID = "i-0123456789abcdef0"
|
||||
FIRST_OBSERVED_AT = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
||||
LAST_SCANNED_AT = datetime(2024, 6, 1, tzinfo=timezone.utc)
|
||||
KEV_DATE_ADDED = datetime(2024, 4, 12, tzinfo=timezone.utc)
|
||||
KEV_DATE_DUE = datetime(2024, 5, 3, tzinfo=timezone.utc)
|
||||
|
||||
# Mocking Calls
|
||||
make_api_call = botocore.client.BaseClient._make_api_call
|
||||
@@ -64,16 +76,83 @@ def mock_make_api_call(self, operation_name, kwargs):
|
||||
"description": "Finding Description",
|
||||
"severity": "MEDIUM",
|
||||
"status": "ACTIVE",
|
||||
"title": "CVE-2022-40897 - setuptools",
|
||||
"title": f"{VULNERABILITY_ID} - setuptools",
|
||||
"type": "PACKAGE_VULNERABILITY",
|
||||
"firstObservedAt": FIRST_OBSERVED_AT,
|
||||
"updatedAt": datetime(2024, 1, 1),
|
||||
"packageVulnerabilityDetails": {
|
||||
"vulnerabilityId": VULNERABILITY_ID
|
||||
},
|
||||
"resources": [{"id": INSTANCE_ID, "type": "AWS_EC2_INSTANCE"}],
|
||||
}
|
||||
]
|
||||
}
|
||||
if operation_name == "ListCoverage":
|
||||
return {
|
||||
"coveredResources": [
|
||||
{
|
||||
"resourceId": INSTANCE_ID,
|
||||
"resourceType": "AWS_EC2_INSTANCE",
|
||||
"accountId": AWS_ACCOUNT_NUMBER,
|
||||
"scanType": "PACKAGE",
|
||||
"scanStatus": {"statusCode": "ACTIVE", "reason": "SUCCESSFUL"},
|
||||
"lastScannedAt": LAST_SCANNED_AT,
|
||||
}
|
||||
]
|
||||
}
|
||||
if operation_name == "BatchGetFindingDetails":
|
||||
return {
|
||||
"findingDetails": [
|
||||
{
|
||||
"findingArn": FINDING_ARN,
|
||||
"cisaData": {
|
||||
"dateAdded": KEV_DATE_ADDED,
|
||||
"dateDue": KEV_DATE_DUE,
|
||||
},
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
return make_api_call(self, operation_name, kwargs)
|
||||
|
||||
|
||||
def mock_make_api_call_finding_details_denied(self, operation_name, kwargs):
|
||||
if operation_name == "BatchGetFindingDetails":
|
||||
raise ClientError(
|
||||
{"Error": {"Code": "AccessDeniedException", "Message": "denied"}},
|
||||
operation_name,
|
||||
)
|
||||
return mock_make_api_call(self, operation_name, kwargs)
|
||||
|
||||
|
||||
def mock_finding_details_error(error_code):
|
||||
def _mock(self, operation_name, kwargs):
|
||||
if operation_name == "BatchGetFindingDetails":
|
||||
return {
|
||||
"findingDetails": [],
|
||||
"errors": [
|
||||
{
|
||||
"findingArn": FINDING_ARN,
|
||||
"errorCode": error_code,
|
||||
"errorMessage": "error",
|
||||
}
|
||||
],
|
||||
}
|
||||
return mock_make_api_call(self, operation_name, kwargs)
|
||||
|
||||
return _mock
|
||||
|
||||
|
||||
def mock_make_api_call_list_denied(self, operation_name, kwargs):
|
||||
if operation_name in ("ListFindings", "ListCoverage"):
|
||||
raise ClientError(
|
||||
{"Error": {"Code": "AccessDeniedException", "Message": "denied"}},
|
||||
operation_name,
|
||||
)
|
||||
return mock_make_api_call(self, operation_name, kwargs)
|
||||
|
||||
|
||||
def mock_generate_regional_clients(provider, service):
|
||||
regional_client = provider._session.current_session.client(
|
||||
service, region_name=AWS_REGION_EU_WEST_1
|
||||
@@ -82,6 +161,29 @@ def mock_generate_regional_clients(provider, service):
|
||||
return {AWS_REGION_EU_WEST_1: regional_client}
|
||||
|
||||
|
||||
def build_inspector(region, vulnerability_ids):
|
||||
return Inspector(
|
||||
id="Inspector2",
|
||||
arn=f"arn:aws:inspector2:{region}:{AWS_ACCOUNT_NUMBER}:inspector2",
|
||||
region=region,
|
||||
status="ENABLED",
|
||||
ec2_status="ENABLED",
|
||||
ecr_status="ENABLED",
|
||||
lambda_status="ENABLED",
|
||||
lambda_code_status="ENABLED",
|
||||
findings=[
|
||||
Finding(
|
||||
arn=f"arn:aws:inspector2:{region}:{AWS_ACCOUNT_NUMBER}:finding/{index}",
|
||||
type="PACKAGE_VULNERABILITY",
|
||||
severity="HIGH",
|
||||
first_observed_at=FIRST_OBSERVED_AT,
|
||||
vulnerability_id=vulnerability_id,
|
||||
)
|
||||
for index, vulnerability_id in enumerate(vulnerability_ids)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# Patch every AWS call using Boto3 and generate_regional_clients to have 1 client
|
||||
@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
|
||||
@patch(
|
||||
@@ -118,3 +220,115 @@ class Test_Inspector2_Service:
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
inspector2 = Inspector2(aws_provider)
|
||||
assert inspector2.inspectors[0].active_findings
|
||||
|
||||
def test_list_findings(self):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
inspector2 = Inspector2(aws_provider)
|
||||
findings = inspector2.inspectors[0].findings
|
||||
assert len(findings) == 1
|
||||
assert findings[0].arn == FINDING_ARN
|
||||
assert findings[0].type == "PACKAGE_VULNERABILITY"
|
||||
assert findings[0].severity == "MEDIUM"
|
||||
assert findings[0].first_observed_at == FIRST_OBSERVED_AT
|
||||
assert findings[0].vulnerability_id == VULNERABILITY_ID
|
||||
assert findings[0].resource_ids == [INSTANCE_ID]
|
||||
|
||||
def test_list_coverage(self):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
inspector2 = Inspector2(aws_provider)
|
||||
coverage = inspector2.inspectors[0].coverage
|
||||
assert len(coverage) == 1
|
||||
assert coverage[0].id == INSTANCE_ID
|
||||
assert (
|
||||
coverage[0].arn
|
||||
== f"arn:aws:ec2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:instance/{INSTANCE_ID}"
|
||||
)
|
||||
assert coverage[0].region == AWS_REGION_EU_WEST_1
|
||||
assert coverage[0].resource_type == "AWS_EC2_INSTANCE"
|
||||
assert coverage[0].scan_type == "PACKAGE"
|
||||
assert coverage[0].scan_status_code == "ACTIVE"
|
||||
assert coverage[0].scan_status_reason == "SUCCESSFUL"
|
||||
assert coverage[0].last_scanned_at == LAST_SCANNED_AT
|
||||
|
||||
def test_list_coverage_keeps_audited_resources(self):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
aws_provider._audit_resources = [
|
||||
f"arn:aws:ec2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:instance/{INSTANCE_ID}"
|
||||
]
|
||||
inspector2 = Inspector2(aws_provider)
|
||||
assert len(inspector2.inspectors[0].coverage) == 1
|
||||
|
||||
def test_list_coverage_skips_non_audited_resources(self):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
aws_provider._audit_resources = [
|
||||
f"arn:aws:ec2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:instance/i-0fedcba9876543210"
|
||||
]
|
||||
inspector2 = Inspector2(aws_provider)
|
||||
assert inspector2.inspectors[0].coverage == []
|
||||
|
||||
def test_batch_get_finding_details(self):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
inspector2 = Inspector2(aws_provider)
|
||||
known_exploited = inspector2.known_exploited_vulnerabilities[VULNERABILITY_ID]
|
||||
assert known_exploited.id == VULNERABILITY_ID
|
||||
assert known_exploited.date_added == KEV_DATE_ADDED
|
||||
assert known_exploited.date_due == KEV_DATE_DUE
|
||||
assert inspector2.vulnerability_lookup_failed == set()
|
||||
|
||||
def test_batch_get_finding_details_denied(self):
|
||||
with patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_make_api_call_finding_details_denied,
|
||||
):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
inspector2 = Inspector2(aws_provider)
|
||||
assert inspector2.known_exploited_vulnerabilities == {}
|
||||
assert inspector2.vulnerability_lookup_failed == {VULNERABILITY_ID}
|
||||
|
||||
def test_finding_details_not_found_is_not_a_lookup_failure(self):
|
||||
with patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_finding_details_error("FINDING_DETAILS_NOT_FOUND"),
|
||||
):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
inspector2 = Inspector2(aws_provider)
|
||||
assert inspector2.known_exploited_vulnerabilities == {}
|
||||
assert inspector2.vulnerability_lookup_failed == set()
|
||||
|
||||
def test_finding_details_error_is_a_lookup_failure(self):
|
||||
with patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_finding_details_error("INTERNAL_ERROR"),
|
||||
):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
inspector2 = Inspector2(aws_provider)
|
||||
assert inspector2.known_exploited_vulnerabilities == {}
|
||||
assert inspector2.vulnerability_lookup_failed == {VULNERABILITY_ID}
|
||||
|
||||
def test_list_findings_and_coverage_denied(self):
|
||||
with patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_make_api_call_list_denied,
|
||||
):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
inspector2 = Inspector2(aws_provider)
|
||||
assert inspector2.inspectors[0].findings is None
|
||||
assert inspector2.inspectors[0].coverage is None
|
||||
assert inspector2.known_exploited_vulnerabilities == {}
|
||||
|
||||
def test_finding_detail_batches_use_one_finding_per_cve(self):
|
||||
vulnerability_ids = [f"CVE-2024-{number:04d}" for number in range(25)]
|
||||
batches = Inspector2._get_finding_detail_batches(
|
||||
[
|
||||
build_inspector(
|
||||
AWS_REGION_EU_WEST_1,
|
||||
vulnerability_ids + vulnerability_ids[:5] + ["GHSA-xxxx-yyyy-zzzz"],
|
||||
),
|
||||
build_inspector(AWS_REGION_US_EAST_1, vulnerability_ids[:3]),
|
||||
]
|
||||
)
|
||||
assert [region for region, _ in batches] == [AWS_REGION_EU_WEST_1] * 3
|
||||
assert [len(findings) for _, findings in batches] == [10, 10, 5]
|
||||
assert sorted(cve for _, findings in batches for _, cve in findings) == sorted(
|
||||
vulnerability_ids
|
||||
)
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
from unittest import mock
|
||||
from unittest.mock import patch
|
||||
|
||||
import botocore
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
SERVER_ID = "s-01234567890abcdef"
|
||||
SERVER_ARN = (
|
||||
f"arn:aws:transfer:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:server/{SERVER_ID}"
|
||||
)
|
||||
CHECK_MODULE = "prowler.providers.aws.services.transfer.transfer_server_fips_security_policy_enabled.transfer_server_fips_security_policy_enabled"
|
||||
|
||||
make_api_call = botocore.client.BaseClient._make_api_call
|
||||
|
||||
|
||||
def mock_server_with_policy(security_policy_name):
|
||||
def _mock(self, operation_name, kwarg):
|
||||
if operation_name == "ListServers":
|
||||
return {"Servers": [{"Arn": SERVER_ARN, "ServerId": SERVER_ID}]}
|
||||
if operation_name == "DescribeServer":
|
||||
return {
|
||||
"Server": {
|
||||
"Arn": SERVER_ARN,
|
||||
"ServerId": SERVER_ID,
|
||||
"Protocols": ["SFTP"],
|
||||
"SecurityPolicyName": security_policy_name,
|
||||
}
|
||||
}
|
||||
return make_api_call(self, operation_name, kwarg)
|
||||
|
||||
return _mock
|
||||
|
||||
|
||||
def execute_check():
|
||||
from prowler.providers.aws.services.transfer.transfer_service import Transfer
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(f"{CHECK_MODULE}.transfer_client", new=Transfer(aws_provider)),
|
||||
):
|
||||
from prowler.providers.aws.services.transfer.transfer_server_fips_security_policy_enabled.transfer_server_fips_security_policy_enabled import (
|
||||
transfer_server_fips_security_policy_enabled,
|
||||
)
|
||||
|
||||
return transfer_server_fips_security_policy_enabled().execute()
|
||||
|
||||
|
||||
class Test_transfer_server_fips_security_policy_enabled:
|
||||
@mock_aws
|
||||
def test_no_servers(self):
|
||||
assert execute_check() == []
|
||||
|
||||
@patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_server_with_policy("TransferSecurityPolicy-FIPS-2025-03"),
|
||||
)
|
||||
@mock_aws
|
||||
def test_fips_policy(self):
|
||||
result = execute_check()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Transfer Server {SERVER_ID} uses FIPS security policy TransferSecurityPolicy-FIPS-2025-03."
|
||||
)
|
||||
assert result[0].resource_id == SERVER_ID
|
||||
assert result[0].resource_arn == SERVER_ARN
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
@patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_server_with_policy("TransferSecurityPolicy-2024-01"),
|
||||
)
|
||||
@mock_aws
|
||||
def test_non_fips_policy(self):
|
||||
result = execute_check()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Transfer Server {SERVER_ID} uses security policy TransferSecurityPolicy-2024-01, which is not a FIPS security policy."
|
||||
)
|
||||
|
||||
@patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_server_with_policy(""),
|
||||
)
|
||||
@mock_aws
|
||||
def test_policy_not_retrieved(self):
|
||||
result = execute_check()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Transfer Server security policies could not be retrieved for {SERVER_ID}; verify the transfer:DescribeServer permission."
|
||||
)
|
||||
assert result[0].resource_id == AWS_ACCOUNT_NUMBER
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
Reference in New Issue
Block a user