From AWS Security Hub to Client-Ready HTML: A Private AI Reporting Pipeline
A developer described a private two-stage pipeline that pulls consolidated AWS Security Hub CSPM findings from GuardDuty and Inspector, renders a first-stage HTML report inside AWS, then post-processes it on a local EC2 host running a self-hosted model to produce client-ready HTML. The workflow keeps inference within the AWS account boundary using an encrypted EBS volume, a narrowly scoped IAM role, and disabled network egress, with remediation approval remaining with humans. The author argues that sending security reports to external AI services creates a new data-processing boundary and that a Markdown extension is not a data classification boundary.
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. 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. https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-internal-providers.html 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 : The result is not “AI fixes security.” It is a repeatable, auditable workflow that reduces report-production effort while keeping remediation approval with people. 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. 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. 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: 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. https://docs.aws.amazon.com/securityhub/latest/userguide/finding-aggregation.html In the AWS console or your reviewed infrastructure-as-code process: RecordState=ACTIVE and 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. example-security-reporting-ap-southeast-1 , with Block Public Access, versioning, default SSE-KMS encryption, a restrictive bucket policy, and lifecycle rules. 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. 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. https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-runtime/client/invoke model.html The Lambda should be useful when Bedrock is unavailable. Its deterministic path should: GetFindings with pagination and a constrained filter set. 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. After deterministic ranking, pass a compact, redacted structured payload to Claude Sonnet 4.6. Ask it to: 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. 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: This distinction is the difference between a credible remediation report and a misleading one. 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. 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. 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. 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. 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. 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. 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. Perform a human review before distribution: Then apply classification, store the final report in the approved location, and record reviewer approval. Preserve the AWS evidence artifact and manifest for auditability. 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: