Security scope: This walkthrough is for authorised defensive reporting in AWS accounts you own or operate. It uses only synthetic names and example paths. Do not place real customer findings, account identifiers, IP addresses, or internal hostnames into public examples.
The problem is not finding collection
Security Hub CSPM provides a useful consolidated security view. It can receive findings from Amazon GuardDuty and Amazon Inspector, and it normalises findings into the AWS Security Finding Format (ASFF). GuardDuty findings and Inspector findings are delivered to Security Hub after the respective integrations are enabled. AWS documents the supported integrations and their regional behaviour.
That still leaves a reporting problem. A weekly operational export can contain vulnerability records, exposures, detections, duplicate resource context, workflow state, identifiers, and raw remediation text. It is valuable evidence, but it is not automatically a decision-ready client report.
This article describes a private two-stage pattern for a hypothetical AWS account in Singapore (ap-southeast-1):
- AWS Security Hub, GuardDuty, and Inspector feed a scheduled Lambda. The Lambda ranks findings deterministically, uses Amazon Bedrock with Claude Sonnet 4.6 for bounded narrative assistance, compares the current weekly snapshot with the previous one, selects six remediation candidates, and writes Markdown plus an evidence-oriented HTML artifact to Amazon S3.
- A controlled ARM64 Kali Linux EC2 instance retrieves only the approved Markdown artifact, runs a local quantised model, and produces a polished offline HTML report designed for client consumption.
The result is not “AI fixes security.” It is a repeatable, auditable workflow that reduces report-production effort while keeping remediation approval with people.
Why use two stages?
The first-stage HTML produced in AWS is useful, but it is usually a poor client-facing experience. It is optimised for traceability back to source findings, not for executive consumption. It tends to contain long ARNs, internal IDs, repeated technical evidence, flattened remediation notes, and no clear distinction between a newly observed finding, a persistent finding, and a verified remediation.
That is not a limitation of Security Hub. It is a consequence of using one artifact for two different jobs:
| Need | Evidence-oriented AWS artifact | Client-ready post-processed HTML |
|---|---|---|
| Preserve source fidelity | Primary purpose | Linked or summarised only |
| Support triage and audit | Strong | Supporting role |
| Explain priority and business action | Limited | Primary purpose |
| Interactive filters and drill-down | Basic or custom-built | Designed in deliberately |
| Safe external distribution | Requires careful review | Explicitly redacted and classified |
The post-processing stage retains traceability but gives the report its own visual hierarchy: executive metrics, severity and provider filters, sortable findings, top affected resources, remediation candidates, and downloadable CSV. It also prevents the report renderer from being coupled to the AWS reporting Lambda.
Why not upload the Markdown to a public Claude or GPT chat?
Do not treat a Markdown extension as a data classification boundary. A security report can include account IDs, resource ARNs, IP addresses, application names, software versions, vulnerability information, detection timing, and an unremediated backlog. Sending it to an external AI service creates a new data-processing boundary, may change retention and access assumptions, and reduces control over the output artifact.
This is not a claim that every managed AI service is unacceptable. An organisation with an approved enterprise agreement, documented retention settings, data-processing assessment, and an approved use case may make a different decision. The point is that this workflow does not require that transfer.
The local post-processing host keeps inference inside the AWS account boundary. The model runs from an encrypted EBS volume on an EC2 instance. The instance has a narrowly scoped IAM role, no public inbound access, approved administrator access through Systems Manager or a restricted bastion, and network egress disabled after the model download. The residual risks are then ordinary AWS workload risks: IAM misuse, instance compromise, EBS access, log leakage, and insecure report distribution. Treat them as such.
Architecture and trust boundaries
GuardDuty + Inspector + Security Hub CSPM
│
▼
Weekly EventBridge schedule → Lambda report builder → S3 (KMS encrypted)
│
deterministic ranking + bounded Bedrock narrative
│
▼
Markdown snapshot + AWS evidence HTML + prior-week state
S3 read-only role → private EC2 local LLM → offline client-ready HTML → S3/client channel
There are two important controls in this design:
- Deterministic logic decides what is high priority and which six items become proposed weekly remediation candidates. The model may explain the result; it must not silently downgrade, suppress, or invent a risk decision.
- The local model receives report data only after optional redaction and never has permissions to Security Hub, GuardDuty, Inspector, remediation APIs, or production workloads.
Scenario 1: deploy the whole solution on a new EC2 host
1. Establish the AWS reporting boundary in Singapore
Use ap-southeast-1 consistently for the hypothetical workload account and the Security Hub aggregation region. Security Hub integrations are regional, and some services or finding types have regional prerequisites. Cross-Region aggregation must be designed deliberately rather than assumed. AWS documents aggregation behaviour separately.
In the AWS console or your reviewed infrastructure-as-code process:
- Enable Security Hub CSPM in Singapore and designate the appropriate organisation administrator if you use AWS Organizations.
- Enable GuardDuty and Amazon Inspector in the same accounts and regions. Confirm their Security Hub integrations are active.
- Decide the reporting scope: only
RecordState=ACTIVEand workflow statuses requiring attention should normally enter the weekly backlog. Keep resolved and suppressed records in the comparison logic, not in the default active queue. - Create a dedicated report bucket, such as
example-security-reporting-ap-southeast-1, with Block Public Access, versioning, default SSE-KMS encryption, a restrictive bucket policy, and lifecycle rules. - Keep source artifacts and final artifacts separate:
s3://example-security-reporting-ap-southeast-1/
weekly/raw/2026-09-07/weekly-security-summary.md
weekly/evidence-html/2026-09-07/security-evidence.html
weekly/client-html/2026-09-07/client-security-report.html
weekly/state/2026-09-07/findings-manifest.json
The findings-manifest.json is important. It is the durable comparison state, containing a stable finding identifier, provider, workflow status, severity, normalised priority score, and report week. Do not compare findings by title alone.
2. Verify the Bedrock model before deploying Lambda
Model availability, model IDs, and cross-Region inference requirements change. Do not hard-code a model identifier copied from a blog. From an identity that is allowed to query Bedrock, verify what the Singapore Region exposes:
aws bedrock list-foundation-models \
--region ap-southeast-1 \
--by-provider Anthropic
Use Claude Sonnet 4.6 only when the account and selected inference mode make it available. If Bedrock returns an inference profile rather than a directly invokable model, configure that profile ID or ARN. The Lambda role needs the appropriate Bedrock inference permission for the approved model or profile. AWS notes that InvokeModel requires bedrock:InvokeModel; its current SDK guidance recommends the Converse API when the model supports it. See the Bedrock runtime API documentation.
3. Build the Lambda as a deterministic report generator first
The Lambda should be useful when Bedrock is unavailable. Its deterministic path should:
- Call Security Hub
GetFindingswith pagination and a constrained filter set. - Remove duplicates using a stable provider finding ID plus affected resource context.
- Calculate an organisation-owned priority score.
- Sort the active backlog and identify the top six proposed remediation items for the next week.
- Load the previous manifest, classify each stable finding as new, persistent, resolved, changed, or not observed, and write a new manifest.
- Render Markdown and a basic evidence HTML artifact from the structured data.
Use an explicit scoring policy. The following is illustrative logic, not a copy-and-paste Lambda implementation:
SEVERITY_BASE = {
"CRITICAL": 100,
"HIGH": 75,
"MEDIUM": 45,
"LOW": 20,
"INFORMATIONAL": 5,
}
def priority_score(finding, asset_criticality=0, exposure_modifier=0):
"""Deterministic, reviewable prioritisation policy."""
return (
SEVERITY_BASE.get(finding["Severity"]["Label"], 0)
+ asset_criticality # for example, 0–25 from approved resource tags
+ exposure_modifier # for example, 0–20 from approved evidence
)
Keep the policy versioned. For Inspector, you may add approved vulnerability context such as exploitability or exposure evidence where it is available in your data model. For GuardDuty, do not assume an Inspector-style CVSS value exists. The model must not manufacture either value.
4. Add Bedrock only for bounded narrative tasks
After deterministic ranking, pass a compact, redacted structured payload to Claude Sonnet 4.6. Ask it to:
- write a concise executive summary;
- explain why each proposed remediation item matters;
- turn approved remediation facts into clear human language; and
- identify data-quality gaps without filling them with guesses.
Constrain the response to JSON and validate it before inserting it into Markdown. A safe contract is:
{
"executive_summary": "string",
"top_six_rationale": [
{"finding_id": "string", "business_rationale": "string", "remediation_summary": "string"}
],
"data_quality_notes": ["string"]
}
The Lambda should fall back to a deterministic summary if Bedrock fails, times out, exceeds a token budget, or returns invalid JSON. Log the error category and request ID, not the full security-report prompt or response.
5. Compare weeks without creating false closure claims
A missing finding is not automatically a fixed finding. It may have been archived, filtered out, delayed, or affected by an integration problem. Report these categories separately:
- New: stable identifier did not exist in the prior manifest.
- Persistent: still active with the same identifier.
- Resolved: source workflow state or verified remediation evidence says resolved.
- Changed: same identifier but a material severity, resource, or workflow change occurred.
- Not observed: absent from the current collection; requires validation before calling it fixed.
This distinction is the difference between a credible remediation report and a misleading one.
6. Schedule and secure the AWS job
Use an EventBridge schedule to invoke the Lambda weekly. The execution role should have only the permissions required to read Security Hub findings, invoke the approved Bedrock model or inference profile, write its report prefix in S3, read the prior manifest prefix, use the designated KMS key, and write CloudWatch Logs.
Do not grant AdministratorAccess, broad s3:*, or broad Bedrock access. Scope S3 permissions to the report bucket and prefixes. If you use a customer-managed KMS key, allow the Lambda role to use it only through S3 for the required bucket. Configure a dead-letter or failure destination and an alarm for failed invocations.
7. Prepare the ARM64 EC2 reporting host
One correction matters here: Kali Linux is Debian-derived; it is not an Ubuntu image. Use an ARM64 Kali AMI if Kali is a requirement. If you start from an Ubuntu ARM64 AMI, keep it as Ubuntu and adapt the hardening baseline accordingly. Do not treat the two images as interchangeable.
For a CPU-only local model, t4g.2xlarge provides 8 vCPUs and 32 GiB RAM. Attach encrypted EBS, use an instance profile rather than access keys, and prefer SSM Session Manager. The instance profile should be read-only to the specific S3 report prefixes and write-only to the final client-report prefix.
Install the local report transformer package and its dependencies:
sudo apt update && sudo apt -y full-upgrade
sudo apt install -y python3 python3-venv python3-dev build-essential cmake curl unzip awscli htop sysstat
sudo useradd --system --home /opt/cloud-report-ai --shell /usr/sbin/nologin cloudreport
sudo mkdir -p /srv/cloud-report-ai/{input,output,checkpoints} /var/log/cloud-report-ai /etc/cloud-report-ai
# Copy the reviewed package to /tmp by an approved internal transfer mechanism.
sudo unzip -q /tmp/cloud_report_ai_arm64_package.zip -d /opt/cloud-report-ai-release
sudo mv /opt/cloud-report-ai-release/cloud_report_ai /opt/cloud-report-ai
sudo rmdir /opt/cloud-report-ai-release
sudo chown -R cloudreport:cloudreport /opt/cloud-report-ai /srv/cloud-report-ai /var/log/cloud-report-ai
cd /opt/cloud-report-ai
sudo -u cloudreport python3 -m venv .venv
sudo -u cloudreport .venv/bin/python -m pip install --upgrade pip wheel
sudo -u cloudreport env CMAKE_ARGS="-DGGML_NATIVE=ON" \
.venv/bin/pip install --no-binary llama-cpp-python -r requirements.txt
The package uses llama-cpp-python, which compiles llama.cpp locally for ARM64. That avoids an x86-only binary dependency. Pin and scan the package release in your own software-supply-chain process before production use.
Download a reviewed quantised model, then remove unrestricted egress when your operating model allows it. For this use case, Qwen2.5 7B Instruct in Q4_K_M GGUF is a practical CPU-only extractor; it is not a replacement for security review.
sudo -u cloudreport mkdir -p /opt/cloud-report-ai/models
sudo -u cloudreport curl -L --fail --retry 3 \
-o /opt/cloud-report-ai/models/Qwen2.5-7B-Instruct-Q4_K_M.gguf \
https://huggingface.co/bartowski/Qwen2.5-7B-Instruct-GGUF/resolve/main/Qwen2.5-7B-Instruct-Q4_K_M.gguf
sudo cp /opt/cloud-report-ai/config.example.json /etc/cloud-report-ai/config.json
sudo chown root:cloudreport /etc/cloud-report-ai/config.json
sudo chmod 640 /etc/cloud-report-ai/config.json
Verify the model checksum from a trusted release record before using it. The external URL above is an operational download location, not a substitute for your supply-chain validation.
8. Retrieve the approved Markdown and create the client report
The EC2 role should retrieve only the intended weekly input. Use an explicit input path, not a wide wildcard across every historic report:
export AWS_REGION=ap-southeast-1
export REPORT_BUCKET=example-security-reporting-ap-southeast-1
export REPORT_WEEK=2026-09-07
export INPUT=/srv/cloud-report-ai/input/weekly-security-summary-${REPORT_WEEK}.md
aws s3 cp \
"s3://${REPORT_BUCKET}/weekly/raw/${REPORT_WEEK}/weekly-security-summary.md" \
"$INPUT" \
--region "$AWS_REGION" \
--only-show-errors
sudo chown cloudreport:cloudreport "$INPUT"
sudo chmod 600 "$INPUT"
cd /opt/cloud-report-ai
sudo -u cloudreport .venv/bin/python ai_report_generator.py \
--config /etc/cloud-report-ai/config.json \
--input "$INPUT" \
--output "/srv/cloud-report-ai/output/client-security-report-${REPORT_WEEK}.html"
The tool chunk-processes long Markdown, normalises findings into a strict schema, records content-hash checkpoints, deduplicates, and writes a self-contained HTML file with no CDN dependency. Its local JSON-repair path prevents a recoverable malformed model response from wasting a long CPU-bound report run; output still requires schema validation.
Upload the reviewed final report to the designated S3 prefix using the bucket's encryption and retention policy, or transfer it through an approved encrypted channel. Do not make an HTML report public merely because it renders locally in a browser.
Scenario 2: run the process again on an existing Kali host
This is the normal weekly operating procedure. The OS, Python environment, local model, package, IAM role, configuration, and output folders already exist. Only the new input Markdown changes.
1. Check the local service before retrieving data
cd /opt/cloud-report-ai
sudo -u cloudreport .venv/bin/python --version
sudo -u cloudreport .venv/bin/python -m pip check
sudo -u cloudreport test -r /opt/cloud-report-ai/models/Qwen2.5-7B-Instruct-Q4_K_M.gguf
free -h
If pip check reports conflicts, stop and resolve them through your change process. Do not upgrade packages automatically in the weekly reporting window.
2. Retrieve and verify only the new weekly input
export AWS_REGION=ap-southeast-1
export REPORT_BUCKET=example-security-reporting-ap-southeast-1
export REPORT_WEEK=2026-09-14
export INPUT=/srv/cloud-report-ai/input/weekly-security-summary-${REPORT_WEEK}.md
aws s3 cp \
"s3://${REPORT_BUCKET}/weekly/raw/${REPORT_WEEK}/weekly-security-summary.md" \
"$INPUT" --region "$AWS_REGION" --only-show-errors
sha256sum "$INPUT"
sudo chown cloudreport:cloudreport "$INPUT"
sudo chmod 600 "$INPUT"
Compare the checksum with a manifest value generated by the AWS reporting stage when your process records one. This confirms you transformed the expected artifact, not merely a file with the expected name.
3. Generate the client HTML report
cd /opt/cloud-report-ai
sudo -u cloudreport .venv/bin/python ai_report_generator.py \
--config /etc/cloud-report-ai/config.json \
--input "$INPUT" \
--output "/srv/cloud-report-ai/output/client-security-report-${REPORT_WEEK}.html"
Monitor the run from a second terminal:
tail -f /var/log/cloud-report-ai/cloud-report-ai.log
On a CPU-only host, a quiet terminal during a chunk is normal: the model emits its response only after generation completes. Check htop or the process CPU usage before treating it as stalled. If CPU is idle for an extended period and the log has not moved, capture the last log lines and investigate the model process, available memory, disk space, and malformed-input handling.
4. Validate before sharing
Perform a human review before distribution:
- Does the total finding count reconcile with the AWS evidence artifact?
- Are the six proposed remediation items valid and owned by the right teams?
- Are “resolved” and “not observed” clearly separated?
- Did redaction remove data that the audience must not receive?
- Do filters, sort order, search, and drill-down work in an offline browser?
- Does the output contain only expected report content and no credentials, object metadata, prompt text, or hidden source files?
Then apply classification, store the final report in the approved location, and record reviewer approval. Preserve the AWS evidence artifact and manifest for auditability.
What this achieves, and what it does not
The tangible achievement is a controlled reporting pipeline with deterministic priority, bounded AI assistance, weekly comparison, and a client-readable presentation layer. It reduces repetitive collection, sorting, drafting, and formatting work while preserving a human decision point for remediation.
Time savings should be measured locally, not presented as a universal benchmark. For example, if a manual weekly collection, prioritisation, writing, and formatting cycle takes four analyst-hours, and the automated pipeline leaves 45–60 minutes of review and approval, the expected saving is roughly three analyst-hours per weekly run. Record actual run time, review time, correction rate, and report acceptance rate for several weeks before making a business case.
The residual limitations remain important:
- The model can omit or misclassify data, so count reconciliation and human review are mandatory.
- A priority score is a policy decision, not an objective truth. Review scoring changes under change control.
- “Top six” means proposed work candidates, not automatically executable remediations.
- A local EC2 model reduces an external data-transfer boundary; it does not remove the need for IAM, EBS, OS, network, and report-distribution controls.
Top comments (0)