Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[aws][fix] Collect and connect Inspector resources properly #2253

Merged
merged 38 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ebaae2d
feat: added collection template to the inspector resource
1101-1 Oct 11, 2024
d8a8142
fix: fixed properties to make it diff
1101-1 Oct 14, 2024
880c7ca
feat: added connect_in_graph to the inspector resources
1101-1 Oct 14, 2024
8f4c488
feat: added tags setting
1101-1 Oct 14, 2024
fe45367
Feat: added resource deletion and tagging/untagging
1101-1 Oct 14, 2024
0110bed
feat: added tests
1101-1 Oct 14, 2024
7ee7b76
revert changes
1101-1 Oct 14, 2024
4bebe89
fixed tests
1101-1 Oct 14, 2024
7d419bd
return function
1101-1 Oct 14, 2024
c4d3722
only keep inspector findings
aquamatthias Oct 15, 2024
8e7b871
feat: added fetching findings for the current account only
1101-1 Oct 15, 2024
4d31061
feat: added changes
1101-1 Oct 17, 2024
a45c222
Merge branch 'main' into km/aws_fix_inspector_collection
1101-1 Oct 17, 2024
a2e4cc1
feat: added provider link
1101-1 Oct 17, 2024
16b5e1c
feat: added more explicit connection
1101-1 Oct 17, 2024
08582f5
fix tests
1101-1 Oct 17, 2024
de35633
Merge branch 'main' into km/aws_fix_inspector_collection
1101-1 Oct 17, 2024
2d46f10
chore: added default case
1101-1 Oct 18, 2024
403a4c6
feat: improved findigs
1101-1 Oct 18, 2024
e50938e
feat: added new collection way for ec2, lambda and ecr
1101-1 Oct 18, 2024
6120e6b
feat: reimplement builder
1101-1 Oct 18, 2024
b6b0e65
feat: added tests and adjust collection
1101-1 Oct 18, 2024
b34bf6d
fixed test
1101-1 Oct 18, 2024
67eddd7
feat: deleted unknown severity
1101-1 Oct 21, 2024
09aca5d
feat: moved to the inspector class
1101-1 Oct 21, 2024
b33890f
feat: added speakable names
1101-1 Oct 21, 2024
dff0441
fix: fixed builder to pass dicationary correctly
1101-1 Oct 21, 2024
0c82504
chore: deleted unnecessary
1101-1 Oct 21, 2024
2186427
feat: reimplemented AssessmentKey
1101-1 Oct 29, 2024
4fd7166
Merge branch 'refs/heads/main' into km/aws_fix_inspector_collection
aquamatthias Oct 30, 2024
1958960
improve code
aquamatthias Oct 30, 2024
ff3f265
imports
aquamatthias Oct 30, 2024
998b9d5
add region as filter for ecr repository
aquamatthias Oct 30, 2024
e3289bd
remove unused properties
aquamatthias Oct 30, 2024
07ba4bb
remove unused import
aquamatthias Oct 30, 2024
2625f11
applied pylint hint
1101-1 Oct 30, 2024
f23af8d
feat: updated test
1101-1 Oct 30, 2024
742ec97
Merge branch 'main' into km/aws_fix_inspector_collection
1101-1 Oct 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion fixlib/fixlib/baseresources.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ class Finding:
severity: Severity = Severity.medium
description: Optional[str] = None
remediation: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
details: Optional[Json] = None


Expand Down Expand Up @@ -409,6 +409,13 @@ def log(self, msg: str, data: Optional[Any] = None, exception: Optional[Exceptio
self.__log.append(log_entry)
self._changes.add("log")

def add_finding(self, provider: str, finding: Finding) -> None:
for assessment in self._assessments:
if assessment.provider == provider:
assessment.findings.append(finding)
return
self._assessments.append(Assessment(provider=provider, findings=[finding]))

def add_change(self, change: str) -> None:
self._changes.add(change)

Expand Down
2 changes: 1 addition & 1 deletion plugins/aws/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ clean-test: ## remove test and coverage artifacts

lint: ## static code analysis
black --line-length 120 --check fix_plugin_aws test
flake8 fix_plugin_aws
flake8 fix_plugin_aws test
mypy --python-version 3.12 --strict --install-types fix_plugin_aws test

test: ## run tests quickly with the default Python
Expand Down
6 changes: 6 additions & 0 deletions plugins/aws/fix_plugin_aws/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
backup,
bedrock,
scp,
inspector,
)
from fix_plugin_aws.resource.base import (
AwsAccount,
Expand Down Expand Up @@ -118,6 +119,7 @@
+ backup.resources
+ amazonq.resources
+ bedrock.resources
+ inspector.resources
)
all_resources: List[Type[AwsResource]] = global_resources + regional_resources

Expand Down Expand Up @@ -244,6 +246,10 @@ def get_last_run() -> Optional[datetime]:
)
shared_queue.wait_for_submitted_work()

# call all registered after collect hooks
for after_collect in global_builder.after_collect_actions:
after_collect()

# connect nodes
log.info(f"[Aws:{self.account.id}] Connect resources and create edges.")
for node, data in list(self.graph.nodes(data=True)):
Expand Down
2 changes: 1 addition & 1 deletion plugins/aws/fix_plugin_aws/resource/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class AwsBackupProtectedResource(AwsResource):
}
api_spec: ClassVar[AwsApiSpec] = AwsApiSpec("backup", "list-protected-resources", "Results")
mapping: ClassVar[Dict[str, Bender]] = {
"id": S("ResourceArn") >> F(lambda arn: arn.rsplit("/")[1]),
"id": S("ResourceArn") >> F(AwsResource.id_from_arn),
"name": S("ResourceName"),
"resource_arn": S("ResourceArn"),
"resource_type": S("ResourceType"),
Expand Down
3 changes: 3 additions & 0 deletions plugins/aws/fix_plugin_aws/resource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ def __init__(
graph_nodes_access: Optional[RWLock] = None,
graph_edges_access: Optional[RWLock] = None,
last_run_started_at: Optional[datetime] = None,
after_collect_actions: Optional[List[Callable[[], Any]]] = None,
) -> None:
self.graph = graph
self.cloud = cloud
Expand All @@ -469,6 +470,7 @@ def __init__(
self.last_run_started_at = last_run_started_at
self.created_at = utc()
self.__builder_cache = {region.safe_name: self}
self.after_collect_actions = after_collect_actions if after_collect_actions is not None else []

if last_run_started_at:
now = utc()
Expand Down Expand Up @@ -705,6 +707,7 @@ def for_region(self, region: AwsRegion) -> GraphBuilder:
self.graph_nodes_access,
self.graph_edges_access,
self.last_run_started_at,
self.after_collect_actions,
)
self.__builder_cache[region.safe_name] = builder
return builder
9 changes: 4 additions & 5 deletions plugins/aws/fix_plugin_aws/resource/ec2.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import base64
from functools import partial
import copy
import logging
from contextlib import suppress
from datetime import datetime, timedelta
from functools import partial
from typing import ClassVar, Dict, Optional, List, Type, Any
import copy

from attrs import define, field
from fix_plugin_aws.aws_client import AwsClient

from fix_plugin_aws.aws_client import AwsClient
from fix_plugin_aws.resource.base import AwsResource, GraphBuilder, AwsApiSpec, get_client
from fix_plugin_aws.resource.cloudwatch import (
AwsCloudwatchQuery,
Expand All @@ -18,9 +18,9 @@
operations_to_iops,
normalizer_factory,
)
from fix_plugin_aws.resource.iam import AwsIamInstanceProfile
from fix_plugin_aws.resource.kms import AwsKmsKey
from fix_plugin_aws.resource.s3 import AwsS3Bucket
from fix_plugin_aws.resource.iam import AwsIamInstanceProfile
from fix_plugin_aws.utils import ToDict, TagsValue
from fixlib.baseresources import (
BaseInstance,
Expand Down Expand Up @@ -49,7 +49,6 @@
from fixlib.json_bender import Bender, S, Bend, ForallBend, bend, MapEnum, F, K, StripNones
from fixlib.types import Json


# region InstanceType
from fixlib.utils import utc

Expand Down
5 changes: 3 additions & 2 deletions plugins/aws/fix_plugin_aws/resource/ecr.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import json
import logging
from typing import ClassVar, Dict, Optional, List, Tuple, Type, Any
from json import loads as json_loads
from typing import ClassVar, Dict, Optional, List, Tuple, Type, Any

from attrs import define, field
from boto3.exceptions import Boto3Error

from fix_plugin_aws.resource.base import AwsResource, AwsApiSpec, GraphBuilder
from fix_plugin_aws.utils import ToDict
from fixlib.baseresources import HasResourcePolicy, PolicySource, PolicySourceKind
from fixlib.baseresources import HasResourcePolicy, ModelReference, PolicySource, PolicySourceKind
from fixlib.json import sort_json
from fixlib.json_bender import Bender, S, Bend
from fixlib.types import Json
Expand All @@ -34,6 +34,7 @@ class AwsEcrRepository(AwsResource, HasResourcePolicy):
_kind_service: ClassVar[Optional[str]] = service_name
_metadata: ClassVar[Dict[str, Any]] = {"icon": "repository", "group": "compute"}
_aws_metadata: ClassVar[Dict[str, Any]] = {"provider_link_tpl": "https://{region_id}.console.aws.amazon.com/ecr/repositories/{name}?region={region}", "arn_tpl": "arn:{partition}:ecr:{region}:{account}:repository/{name}"} # fmt: skip
_reference_kinds: ClassVar[ModelReference] = {}
api_spec: ClassVar[AwsApiSpec] = AwsApiSpec("ecr", "describe-repositories", "repositories")
public_spec: ClassVar[AwsApiSpec] = AwsApiSpec("ecr-public", "describe-repositories", "repositories")
mapping: ClassVar[Dict[str, Bender]] = {
Expand Down
187 changes: 187 additions & 0 deletions plugins/aws/fix_plugin_aws/resource/inspector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import logging
from datetime import datetime
from functools import partial
from typing import ClassVar, Dict, Optional, List, Tuple, Type, Any

from attrs import define, field
from boto3.exceptions import Boto3Error

from fix_plugin_aws.resource.base import AwsResource, AwsApiSpec, GraphBuilder
from fix_plugin_aws.resource.ec2 import AwsEc2Instance
from fix_plugin_aws.resource.ecr import AwsEcrRepository
from fix_plugin_aws.resource.lambda_ import AwsLambdaFunction
from fixlib.baseresources import PhantomBaseResource, Severity, Finding
from fixlib.json_bender import Bender, S, ForallBend, Bend, F
from fixlib.types import Json

log = logging.getLogger("fix.plugins.aws")
service_name = "inspector2"

amazon_inspector = "amazon_inspector"


@define(eq=False, slots=False)
class AwsInspectorRecommendation:
kind: ClassVar[str] = "aws_inspector_recommendation"
mapping: ClassVar[Dict[str, Bender]] = {"url": S("Url"), "text": S("text")}
url: Optional[str] = field(default=None, metadata={"description": "The URL address to the CVE remediation recommendations."}) # fmt: skip
text: Optional[str] = field(default=None, metadata={"description": "The recommended course of action to remediate the finding."}) # fmt: skip


@define(eq=False, slots=False)
class AwsInspectorRemediation:
kind: ClassVar[str] = "aws_inspector_remediation"
mapping: ClassVar[Dict[str, Bender]] = {
"recommendation": S("recommendation") >> Bend(AwsInspectorRecommendation.mapping)
}
recommendation: Optional[AwsInspectorRecommendation] = field(default=None, metadata={"description": "An object that contains information about the recommended course of action to remediate the finding."}) # fmt: skip


@define(eq=False, slots=False)
class AwsInspectorResource:
kind: ClassVar[str] = "aws_inspector_resource"
mapping: ClassVar[Dict[str, Bender]] = {
# "details": S("details") # not used
"id": S("id"),
"partition": S("partition"),
"region": S("region"),
"type": S("type"),
}
id: Optional[str] = field(default=None, metadata={"description": "The ID of the resource."}) # fmt: skip
partition: Optional[str] = field(default=None, metadata={"description": "The partition of the resource."}) # fmt: skip
region: Optional[str] = field(default=None, metadata={"description": "The Amazon Web Services Region the impacted resource is located in."}) # fmt: skip
type: Optional[str] = field(default=None, metadata={"description": "The type of resource."}) # fmt: skip


@define(eq=False, slots=False)
class AwsInspectorFinding(AwsResource, PhantomBaseResource):
kind: ClassVar[str] = "aws_inspector_finding"
api_spec: ClassVar[AwsApiSpec] = AwsApiSpec(service_name, "list-findings")
_model_export: ClassVar[bool] = False # do not export this class, since there will be no instances of it
mapping: ClassVar[Dict[str, Bender]] = {
"id": S("findingArn") >> F(AwsResource.id_from_arn),
"name": S("title"),
"mtime": S("updatedAt"),
"arn": S("findingArn"),
"aws_account_id": S("awsAccountId"),
"description": S("description"),
"epss": S("epss", "score"),
"exploit_available": S("exploitAvailable"),
"exploitability_details": S("exploitabilityDetails", "lastKnownExploitAt"),
"finding_arn": S("findingArn"),
"first_observed_at": S("firstObservedAt"),
"fix_available": S("fixAvailable"),
"inspector_score": S("inspectorScore"),
"last_observed_at": S("lastObservedAt"),
"remediation": S("remediation") >> Bend(AwsInspectorRemediation.mapping),
"finding_resources": S("resources", default=[]) >> ForallBend(AwsInspectorResource.mapping),
"finding_severity": S("severity"),
"status": S("status"),
"title": S("title"),
"type": S("type"),
"updated_at": S("updatedAt"),
# available but not used properties:
# "inspector_score_details": S("inspectorScoreDetails")
# "code_vulnerability_details": S("codeVulnerabilityDetails")
# "network_reachability_details": S("networkReachabilityDetails")
# "package_vulnerability_details": S("packageVulnerabilityDetails")
}
aws_account_id: Optional[str] = field(default=None, metadata={"description": "The Amazon Web Services account ID associated with the finding."}) # fmt: skip
description: Optional[str] = field(default=None, metadata={"description": "The description of the finding."}) # fmt: skip
epss: Optional[float] = field(default=None, metadata={"description": "The finding's EPSS score."}) # fmt: skip
exploit_available: Optional[str] = field(default=None, metadata={"description": "If a finding discovered in your environment has an exploit available."}) # fmt: skip
exploitability_details: Optional[datetime] = field(default=None, metadata={"description": "The details of an exploit available for a finding discovered in your environment."}) # fmt: skip
finding_arn: Optional[str] = field(default=None, metadata={"description": "The Amazon Resource Number (ARN) of the finding."}) # fmt: skip
first_observed_at: Optional[datetime] = field(default=None, metadata={"description": "The date and time that the finding was first observed."}) # fmt: skip
fix_available: Optional[str] = field(default=None, metadata={"description": "Details on whether a fix is available through a version update. This value can be YES, NO, or PARTIAL. A PARTIAL fix means that some, but not all, of the packages identified in the finding have fixes available through updated versions."}) # fmt: skip
inspector_score: Optional[float] = field(default=None, metadata={"description": "The Amazon Inspector score given to the finding."}) # fmt: skip
last_observed_at: Optional[datetime] = field(default=None, metadata={"description": "The date and time the finding was last observed. This timestamp for this field remains unchanged until a finding is updated."}) # fmt: skip
remediation: Optional[AwsInspectorRemediation] = field(default=None, metadata={"description": "An object that contains the details about how to remediate a finding."}) # fmt: skip
finding_resources: Optional[List[AwsInspectorResource]] = field(factory=list, metadata={"description": "Contains information on the resources involved in a finding. The resource value determines the valid values for type in your request. For more information, see Finding types in the Amazon Inspector user guide."}) # fmt: skip
finding_severity: Optional[str] = field(default=None, metadata={"description": "The severity of the finding. UNTRIAGED applies to PACKAGE_VULNERABILITY type findings that the vendor has not assigned a severity yet. For more information, see Severity levels for findings in the Amazon Inspector user guide."}) # fmt: skip
status: Optional[str] = field(default=None, metadata={"description": "The status of the finding."}) # fmt: skip
title: Optional[str] = field(default=None, metadata={"description": "The title of the finding."}) # fmt: skip
type: Optional[str] = field(default=None, metadata={"description": "The type of the finding. The type value determines the valid values for resource in your request. For more information, see Finding types in the Amazon Inspector user guide."}) # fmt: skip
updated_at: Optional[datetime] = field(default=None, metadata={"description": "The date and time the finding was last updated at."}) # fmt: skip

def parse_finding(self, source: Json) -> Finding:
severity_mapping = {
"INFORMATIONAL": Severity.info,
"LOW": Severity.low,
"MEDIUM": Severity.medium,
"HIGH": Severity.high,
"CRITICAL": Severity.critical,
}
finding_title = self.safe_name
if not self.finding_severity:
finding_severity = Severity.medium
else:
finding_severity = severity_mapping.get(self.finding_severity, Severity.medium)
description = self.description
remediation = ""
if self.remediation and self.remediation.recommendation:
remediation = self.remediation.recommendation.text or ""
updated_at = self.updated_at
details = source.get("packageVulnerabilityDetails", {}) | source.get("codeVulnerabilityDetails", {})
return Finding(finding_title, finding_severity, description, remediation, updated_at, details)

@classmethod
def collect_resources(cls, builder: GraphBuilder) -> None:
def check_type_and_adjust_id(
class_type: Optional[str], resource_id: Optional[str]
) -> Tuple[Optional[Type[Any]], Optional[Dict[str, Any]]]:
if not resource_id or not class_type:
return None, None
match class_type:
case "AWS_LAMBDA_FUNCTION":
# remove lambda's version from arn
lambda_arn = resource_id.rsplit(":", 1)[0]
return AwsLambdaFunction, {"arn": lambda_arn}
case "AWS_EC2_INSTANCE":
return AwsEc2Instance, {"id": resource_id}
case "AWS_ECR_REPOSITORY":
return AwsEcrRepository, {"id": resource_id, "_region": builder.region}
case _:
return None, None

def add_finding(
provider: str, finding: Finding, clazz: Optional[Type[AwsResource]] = None, **node: Any
) -> None:
if resource := builder.node(clazz=clazz, **node):
resource.add_finding(provider, finding)

# Default behavior: in case the class has an ApiSpec, call the api and call collect.
log.debug(f"Collecting {cls.__name__} in region {builder.region.name}")
try:
for item in builder.client.list(
aws_service=service_name,
action="list-findings",
result_name="findings",
expected_errors=["AccessDeniedException"],
filterCriteria={"awsAccountId": [{"comparison": "EQUALS", "value": f"{builder.account.id}"}]},
):
if finding := AwsInspectorFinding.from_api(item, builder):
for fr in finding.finding_resources or []:
clazz, res_filter = check_type_and_adjust_id(fr.type, fr.id)
if clazz and res_filter:
# append the finding when all resources have been collected
builder.after_collect_actions.append(
partial(
add_finding,
amazon_inspector,
finding.parse_finding(item),
clazz,
**res_filter,
)
)
except Boto3Error as e:
msg = f"Error while collecting {cls.__name__} in region {builder.region.name}: {e}"
builder.core_feedback.error(msg, log)
raise
except Exception as e:
msg = f"Error while collecting {cls.__name__} in region {builder.region.name}: {e}"
builder.core_feedback.info(msg, log)
raise


resources: List[Type[AwsResource]] = [AwsInspectorFinding]
5 changes: 3 additions & 2 deletions plugins/aws/fix_plugin_aws/resource/lambda_.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from datetime import timedelta
import json as json_p
import logging
import re
from datetime import timedelta
from typing import ClassVar, Dict, Optional, List, Tuple, Type, Any

from attrs import define, field

from fix_plugin_aws.aws_client import AwsClient
from fix_plugin_aws.resource.base import AwsResource, GraphBuilder, AwsApiSpec, parse_json
from fix_plugin_aws.resource.cloudwatch import AwsCloudwatchQuery, normalizer_factory
Expand All @@ -19,9 +20,9 @@
PolicySourceKind,
)
from fixlib.graph import Graph
from fixlib.json import sort_json
from fixlib.json_bender import Bender, S, Bend, ForallBend, F, bend
from fixlib.types import Json
from fixlib.json import sort_json

log = logging.getLogger("fix.plugins.aws")

Expand Down
5 changes: 5 additions & 0 deletions plugins/aws/test/collector_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ def count_kind(clazz: Type[AwsResource]) -> int:
return count

for resource in all_resources:
# there will be no instances of resources that are not exported
if not resource._model_export:
continue
assert count_kind(resource) > 0, f"No instances of {resource.__name__} found"

# make sure all threads have been joined
Expand Down Expand Up @@ -106,6 +109,8 @@ def all_base_classes(cls: Type[Any]) -> Set[Type[Any]]:
expected_declared_properties = ["kind", "_kind_display"]
expected_props_in_hierarchy = ["_kind_service", "_metadata"]
for rc in all_resources:
if not rc._model_export:
continue
for prop in expected_declared_properties:
assert prop in rc.__dict__, f"{rc.__name__} missing {prop}"
with_bases = (all_base_classes(rc) | {rc}) - {AwsResource, BaseResource}
Expand Down
2 changes: 2 additions & 0 deletions plugins/aws/test/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ def build_graph(
for cls in clazz if isinstance(clazz, list) else [clazz]:
cls.collect_resources(builder)
builder.executor.wait_for_submitted_work()
for after_collect in builder.after_collect_actions:
after_collect()
return builder


Expand Down
Loading
Loading