awsfindingsmanagerlib 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ """The main package of awsfindingsmanagerlib."""
2
+
3
+ from .awsfindingsmanagerlib import validate_filters, validate_updates
4
+ from .boto_utils import (
5
+ BotoStubCall,
6
+ get_values_by_boto_argument,
7
+ stub_boto_client,
8
+ validate_call_params,
9
+ )
10
+ from .manager import Manager, MatchAndUpdateResult
11
+ from .note_text_config import NoteTextConfig
12
+ from .rule import Rule
13
+ from .utils import (
14
+ TimeRange,
15
+ are_keys_in_collection,
16
+ are_keys_in_dataclass_fields,
17
+ is_valid_against_reference,
18
+ parse_timestamp_str_if_set,
19
+ )
20
+
21
+ __all__ = [
22
+ "BotoStubCall",
23
+ "Manager",
24
+ "MatchAndUpdateResult",
25
+ "NoteTextConfig",
26
+ "Rule",
27
+ "TimeRange",
28
+ "are_keys_in_collection",
29
+ "are_keys_in_dataclass_fields",
30
+ "get_values_by_boto_argument",
31
+ "is_valid_against_reference",
32
+ "parse_timestamp_str_if_set",
33
+ "stub_boto_client",
34
+ "validate_call_params",
35
+ "validate_filters",
36
+ "validate_updates",
37
+ ]
@@ -0,0 +1 @@
1
+ """AWS Lambda entry points for running awsfindingsmanagerlib as part of an event-driven pipeline."""
@@ -0,0 +1,28 @@
1
+ """Lazily-created, cached boto3 clients shared across Lambda handlers.
2
+
3
+ Creating these lazily (rather than as module-level constants) means importing a handler module
4
+ never requires AWS credentials or network access, which keeps the handlers unit-testable.
5
+ """
6
+
7
+ from functools import cache
8
+
9
+ import boto3
10
+ from botocore.client import BaseClient
11
+
12
+
13
+ @cache
14
+ def get_securityhub_client() -> BaseClient:
15
+ """Return a cached boto3 Security Hub client, created on first use."""
16
+ return boto3.client("securityhub")
17
+
18
+
19
+ @cache
20
+ def get_s3_client() -> BaseClient:
21
+ """Return a cached boto3 S3 client, created on first use."""
22
+ return boto3.client("s3")
23
+
24
+
25
+ @cache
26
+ def get_sqs_client() -> BaseClient:
27
+ """Return a cached boto3 SQS client, created on first use."""
28
+ return boto3.client("sqs")
@@ -0,0 +1,74 @@
1
+ """EventBridge-triggered Lambda handler.
2
+
3
+ Suppresses findings carried by a "Security Hub Findings - Imported" EventBridge event.
4
+
5
+ Returns `{"finding_state": "suppressed"}` or `{"finding_state": "skipped"}` and never raises, so
6
+ that a Step Function orchestrating this handler ahead of a ticketing integration can branch on
7
+ the result: `terraform-aws-mcaf-securityhub-findings-manager` relies on exactly this contract to
8
+ decide whether a finding still needs a Jira ticket after this handler has run.
9
+ """
10
+
11
+ from typing import Any
12
+
13
+ from awsfindingsmanagerlib import Manager
14
+ from awsfindingsmanagerlib.aws_lambda.clients import (
15
+ get_s3_client,
16
+ get_securityhub_client,
17
+ )
18
+ from awsfindingsmanagerlib.aws_lambda.logging import get_logger
19
+ from awsfindingsmanagerlib.aws_lambda.rules_backend import load_rules
20
+
21
+ LOGGER = get_logger()
22
+
23
+
24
+ @LOGGER.inject_lambda_context()
25
+ def lambda_handler(event: dict[str, Any], _context: object) -> dict[str, str]:
26
+ """Suppress the findings carried by an EventBridge "Security Hub Findings - Imported" event.
27
+
28
+ Parameters
29
+ ----------
30
+ event : dict[str, Any]
31
+ The EventBridge event. `event["detail"]["findings"]` is expected to be a list of Security
32
+ Hub finding dicts.
33
+ _context : object
34
+ The Lambda context object (unused).
35
+
36
+ Returns
37
+ -------
38
+ dict[str, str]
39
+ `{"finding_state": "suppressed"}` if at least one finding matched a rule and all matching
40
+ updates were processed successfully, `{"finding_state": "skipped"}` otherwise (no rule
41
+ matched, the manager could not be initialized, or an update failed).
42
+ """
43
+ findings = event.get("detail", {}).get("findings", [])
44
+
45
+ try:
46
+ manager = Manager.from_rules_document(
47
+ load_rules(s3_client=get_s3_client()), get_securityhub_client()
48
+ )
49
+ except Exception:
50
+ LOGGER.exception("Failed to initialize the findings manager; skipping.")
51
+ return {"finding_state": "skipped"}
52
+
53
+ matched_any = False
54
+ processed_all = True
55
+
56
+ for finding in findings:
57
+ try:
58
+ result = manager.process_finding(finding)
59
+ except Exception:
60
+ LOGGER.exception("Failed to process a finding; skipping it.")
61
+ processed_all = False
62
+ continue
63
+
64
+ matched_any = matched_any or result.matched_rules > 0
65
+ processed_all = processed_all and result.all_processed
66
+
67
+ if matched_any and processed_all:
68
+ LOGGER.info("Successfully suppressed matching finding(s).")
69
+ return {"finding_state": "suppressed"}
70
+
71
+ LOGGER.info(
72
+ "No finding(s) were suppressed (no rule matched, or an update could not be processed)."
73
+ )
74
+ return {"finding_state": "skipped"}
@@ -0,0 +1,36 @@
1
+ """Shared Lambda logging setup, built on aws-lambda-powertools.
2
+
3
+ aws-lambda-powertools is an optional dependency (the 'lambda' extra), so importing this module
4
+ without it installed raises a clear, actionable error instead of a bare ModuleNotFoundError.
5
+ """
6
+
7
+ try:
8
+ from aws_lambda_powertools import Logger
9
+ from aws_lambda_powertools.logging import utils
10
+ except ImportError as error:
11
+ msg = (
12
+ "aws-lambda-powertools is required to use awsfindingsmanagerlib.aws_lambda handlers. "
13
+ "Install it with the 'lambda' extra, e.g. `uv add 'awsfindingsmanagerlib[lambda]'` "
14
+ "or `pip install 'awsfindingsmanagerlib[lambda]'`."
15
+ )
16
+ raise ImportError(msg) from error
17
+
18
+
19
+ def get_logger() -> Logger:
20
+ """Create a powertools Logger and propagate its configuration to other registered loggers.
21
+
22
+ The service name is taken from the `POWERTOOLS_SERVICE_NAME` environment variable, as usual
23
+ for aws-lambda-powertools. Handlers built on this logger call `inject_lambda_context()`
24
+ without an explicit `log_event` argument, so whether the incoming event is logged (which can
25
+ include full Security Hub finding payloads) is controlled entirely by the
26
+ `POWERTOOLS_LOGGER_LOG_EVENT` environment variable (defaults to `false`); passing
27
+ `log_event=True`/`False` explicitly at the call site would silently override that setting.
28
+
29
+ Returns
30
+ -------
31
+ Logger
32
+ A ready-to-use powertools Logger.
33
+ """
34
+ logger = Logger()
35
+ utils.copy_config_to_registered_loggers(source_logger=logger)
36
+ return logger
@@ -0,0 +1,61 @@
1
+ """Load suppression rules for AWS Lambda use."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from botocore.client import BaseClient
8
+
9
+ try:
10
+ from yaml import safe_load
11
+ except ImportError as error:
12
+ msg = (
13
+ "pyyaml is required to use awsfindingsmanagerlib.aws_lambda handlers. "
14
+ "Install it with the 'lambda' extra, e.g. `uv add 'awsfindingsmanagerlib[lambda]'` "
15
+ "or `pip install 'awsfindingsmanagerlib[lambda]'`."
16
+ )
17
+ raise ImportError(msg) from error
18
+
19
+
20
+ def load_rules(s3_client: BaseClient | None = None) -> dict[str, Any]:
21
+ """Load the rules document, from S3 if configured, otherwise from a local file.
22
+
23
+ If both the `S3_BUCKET_NAME` and `S3_OBJECT_NAME` environment variables are set, the rules
24
+ document is loaded from that S3 object. Otherwise, it is loaded from the local file at
25
+ `RULES_PATH` (defaults to `rules.yaml`).
26
+
27
+ Parameters
28
+ ----------
29
+ s3_client : BaseClient, optional
30
+ The boto3 S3 client to use when loading rules from S3. Only used (and required) when
31
+ `S3_BUCKET_NAME` and `S3_OBJECT_NAME` are both set. Accepting it as a parameter, rather
32
+ than creating one internally, keeps this function free of AWS calls in unit tests.
33
+
34
+ Returns
35
+ -------
36
+ dict[str, Any]
37
+ The parsed rules document, expected to contain a top-level `Rules` key and optionally a
38
+ `ManagerConfig` key. See `awsfindingsmanagerlib.Manager.from_rules_document` for turning this into a
39
+ ready-to-use `Manager`.
40
+
41
+ Raises
42
+ ------
43
+ ValueError
44
+ If S3 loading is configured but no `s3_client` was provided.
45
+ """
46
+ bucket_name = os.environ.get("S3_BUCKET_NAME")
47
+ object_name = os.environ.get("S3_OBJECT_NAME")
48
+
49
+ if bucket_name and object_name:
50
+ if s3_client is None:
51
+ msg = (
52
+ "S3_BUCKET_NAME and S3_OBJECT_NAME are set, but no s3_client was provided "
53
+ "to load_rules()."
54
+ )
55
+ raise ValueError(msg)
56
+ response = s3_client.get_object(Bucket=bucket_name, Key=object_name)
57
+ return safe_load(response["Body"].read())
58
+
59
+ rules_path = os.environ.get("RULES_PATH", "rules.yaml")
60
+ with Path(rules_path).open() as file:
61
+ return safe_load(file)
@@ -0,0 +1,42 @@
1
+ """Standalone scheduled Lambda handler.
2
+
3
+ Loads the configured rules (from S3, or a local file bundled in the deployment package) and
4
+ applies all of them against every currently matching Security Hub finding. Intended to be run on
5
+ a schedule (e.g. via an EventBridge rule) rather than triggered by individual finding or rules
6
+ events; see `awsfindingsmanagerlib.aws_lambda.events`, `.trigger` and `.worker` for the event-driven pipeline
7
+ used by `terraform-aws-mcaf-securityhub-findings-manager`.
8
+ """
9
+
10
+ from typing import Any
11
+
12
+ from awsfindingsmanagerlib import Manager
13
+ from awsfindingsmanagerlib.aws_lambda.clients import (
14
+ get_s3_client,
15
+ get_securityhub_client,
16
+ )
17
+ from awsfindingsmanagerlib.aws_lambda.logging import get_logger
18
+ from awsfindingsmanagerlib.aws_lambda.rules_backend import load_rules
19
+
20
+ LOGGER = get_logger()
21
+
22
+
23
+ @LOGGER.inject_lambda_context()
24
+ def lambda_handler(_event: dict[str, Any], _context: object) -> None:
25
+ """Apply all configured suppression rules to every currently matching Security Hub finding.
26
+
27
+ Raises
28
+ ------
29
+ RuntimeError
30
+ If not all matched findings could be successfully processed. Check the logged warnings
31
+ above the raised error for which finding(s)/rule(s) were affected.
32
+ """
33
+ manager = Manager.from_rules_document(
34
+ load_rules(s3_client=get_s3_client()), get_securityhub_client()
35
+ )
36
+
37
+ if not manager.get_and_update_all():
38
+ msg = (
39
+ "Not all findings were processed successfully; "
40
+ "see the warnings logged above for details."
41
+ )
42
+ raise RuntimeError(msg)
@@ -0,0 +1,48 @@
1
+ """S3-triggered Lambda handler.
2
+
3
+ Fired when the rules document is uploaded to S3. Loads the rules and fans each one out as its
4
+ own message on SQS, carrying the shared `ManagerConfig` alongside it, so that the worker Lambda
5
+ (`awsfindingsmanagerlib.aws_lambda.worker`) can build an equivalent single-rule Manager for it.
6
+
7
+ Unlike `events.lambda_handler`, failures here are raised rather than swallowed: there is no
8
+ downstream Step Function branching on this handler's result, and a failure to queue rules should
9
+ be visible (e.g. via a Lambda error metric alarm) rather than silently skipped.
10
+ """
11
+
12
+ import json
13
+ import os
14
+ from typing import Any
15
+
16
+ from awsfindingsmanagerlib.aws_lambda.clients import get_s3_client, get_sqs_client
17
+ from awsfindingsmanagerlib.aws_lambda.logging import get_logger
18
+ from awsfindingsmanagerlib.aws_lambda.rules_backend import load_rules
19
+
20
+ LOGGER = get_logger()
21
+
22
+
23
+ @LOGGER.inject_lambda_context()
24
+ def lambda_handler(_event: dict[str, Any], _context: object) -> None:
25
+ """Load the configured rules and place each one on SQS for the worker Lambda to apply.
26
+
27
+ Reads the destination queue URL from the `SQS_QUEUE_URL` environment variable.
28
+
29
+ Raises
30
+ ------
31
+ Exception
32
+ Re-raises any failure to load the rules or send a message to SQS, after logging it.
33
+ """
34
+ try:
35
+ rules = load_rules(s3_client=get_s3_client())
36
+ manager_config = rules.get("ManagerConfig", {})
37
+ queue_url = os.environ.get("SQS_QUEUE_URL")
38
+ sqs = get_sqs_client()
39
+
40
+ for rule in rules.get("Rules", []):
41
+ message_body = json.dumps(
42
+ {"ManagerConfig": manager_config, "Rules": [rule]}
43
+ )
44
+ LOGGER.info("Putting rule on SQS. Rule details: %s", message_body)
45
+ sqs.send_message(QueueUrl=queue_url, MessageBody=message_body)
46
+ except Exception:
47
+ LOGGER.exception("Failed putting rule(s) on SQS.")
48
+ raise
@@ -0,0 +1,60 @@
1
+ """SQS-triggered Lambda handler.
2
+
3
+ Each SQS record carries a single-rule rules document (as produced by
4
+ `awsfindingsmanagerlib.aws_lambda.trigger`): applies that rule against all currently matching Security Hub
5
+ findings.
6
+
7
+ Failures are reported per record via SQS's partial batch response contract
8
+ (`{"batchItemFailures": [...]}`), not by raising: a failed record becomes visible again after the
9
+ queue's visibility timeout and is retried (and eventually sent to the dead-letter queue per the
10
+ queue's redrive policy) without the whole batch, including already-successful records, being
11
+ reprocessed. This requires `function_response_types = ["ReportBatchItemFailures"]` to be set on
12
+ the event source mapping; without it, Lambda ignores this return value and deletes the entire
13
+ batch regardless of these results.
14
+ """
15
+
16
+ import json
17
+ from typing import Any
18
+
19
+ from awsfindingsmanagerlib import Manager
20
+ from awsfindingsmanagerlib.aws_lambda.clients import get_securityhub_client
21
+ from awsfindingsmanagerlib.aws_lambda.logging import get_logger
22
+
23
+ LOGGER = get_logger()
24
+
25
+
26
+ @LOGGER.inject_lambda_context()
27
+ def lambda_handler(
28
+ event: dict[str, Any], _context: object
29
+ ) -> dict[str, list[dict[str, str]]]:
30
+ """Apply the suppression rule carried by each SQS record to matching Security Hub findings.
31
+
32
+ Parameters
33
+ ----------
34
+ event : dict[str, Any]
35
+ The SQS event. Each `event["Records"]` entry's `body` is expected to be a JSON-encoded
36
+ rules document with exactly one rule in its `Rules` list.
37
+ _context : object
38
+ The Lambda context object (unused).
39
+
40
+ Returns
41
+ -------
42
+ dict[str, list[dict[str, str]]]
43
+ `{"batchItemFailures": [{"itemIdentifier": <messageId>}, ...]}`, naming only the records
44
+ that failed to process. Requires `function_response_types = ["ReportBatchItemFailures"]`
45
+ on the SQS event source mapping to take effect.
46
+ """
47
+ batch_item_failures: list[dict[str, str]] = []
48
+
49
+ for record in event.get("Records", []):
50
+ try:
51
+ rule = json.loads(record["body"])
52
+ manager = Manager.from_rules_document(rule, get_securityhub_client())
53
+ manager.get_and_update_all()
54
+ except Exception:
55
+ LOGGER.exception(
56
+ "Failed to process rule from SQS record %s.", record.get("messageId")
57
+ )
58
+ batch_item_failures.append({"itemIdentifier": record["messageId"]})
59
+
60
+ return {"batchItemFailures": batch_item_failures}
@@ -0,0 +1,75 @@
1
+ """The main module of awsfindingsmanagerlib."""
2
+
3
+ import botocore.session
4
+ from botocore.client import BaseClient
5
+
6
+ from .boto_utils import (
7
+ BotoStubCall,
8
+ validate_call_params,
9
+ )
10
+
11
+
12
+ def _validate_call_params(
13
+ stub_responses: list[BotoStubCall],
14
+ session_client: BaseClient | None = None,
15
+ ) -> None:
16
+ if not session_client:
17
+ session_client = botocore.session.get_session().create_client("securityhub")
18
+
19
+ validate_call_params(stub_responses, session_client)
20
+
21
+
22
+ def validate_filters(filters: dict, session_client: BaseClient | None = None) -> None:
23
+ """Validate AWS SecurityHub filters to get findings.
24
+
25
+ Parameters
26
+ ----------
27
+ filters : dict
28
+ The filters to validate
29
+ session_client : BaseClient, optional
30
+ A boto session BaseClient for AWS SecurityHub
31
+ Tries to create one if not provided
32
+
33
+ Raises
34
+ ------
35
+ botocore.exceptions.ParamValidationError
36
+ If the filters contain invalid values
37
+ """
38
+ _validate_call_params(
39
+ [
40
+ BotoStubCall(
41
+ method="get_findings",
42
+ service_response={"Findings": []},
43
+ expected_params={"Filters": filters},
44
+ )
45
+ ],
46
+ session_client,
47
+ )
48
+
49
+
50
+ def validate_updates(updates: dict, session_client: BaseClient | None = None) -> None:
51
+ """Validate AWS SecurityHub updates to findings.
52
+
53
+ Parameters
54
+ ----------
55
+ updates : dict
56
+ The updates to make to (a set of) findings
57
+ session_client : BaseClient, optional
58
+ A boto session BaseClient for AWS SecurityHub
59
+ Tries to create one if not provided
60
+
61
+ Raises
62
+ ------
63
+ botocore.exceptions.ParamValidationError
64
+ If the updates contain invalid values
65
+ """
66
+ _validate_call_params(
67
+ [
68
+ BotoStubCall(
69
+ method="batch_update_findings",
70
+ service_response={"ProcessedFindings": [], "UnprocessedFindings": []},
71
+ expected_params=updates,
72
+ )
73
+ ],
74
+ session_client,
75
+ )