Skip to content

Integrate

The repository supports two parallel deployments:

  • Azure terraform/: the existing public VM, SSH workflow, broad suite catalog, and Azure-shaped outputs remain supported.
  • AWS terraform/aws/: a public-subnet EC2 worker for the canonical csd-violations browser suite, with jumpbox-restricted SSH and Systems Manager recovery.

Each root owns independent state and resources. Never copy state, plans, variable files, output assumptions, or destroy commands between them.

GitHub jumpbox -> restricted SSH /32 -> Elastic IP -> public AWS worker
|
+-> F5 XC HTTP LB -> AWS Juice Shop origin
+-> SSM recovery and constrained runs
+-> encrypted S3 evidence
+-> encrypted CloudWatch logs

The AWS worker targets the exact authorized URL https://client-side-defense.f5-sales-demo.com. It runs headed Chrome under Xvfb without exposing Chrome DevTools. Terraform assigns an explicit Elastic IP while keeping associate_public_ip_address = false, and permits TCP/22 only from the revalidated operator_ssh_cidr. The private SSH key remains outside Terraform state.

After applying the independently reviewed AWS saved plan, use these exact non-sensitive Terraform outputs:

OutputIntegration use
instance_idTarget for Systems Manager Run Command and status inspection
public_ipExplicit worker Elastic IP
ssh_commandUbuntu SSH command with the operator key-path hint
ssm_document_nameSubmit one of the 11 stable scenario names through Scenario
evidence_bucket_nameRetrieve private screenshots, receipt.json, and SHA256SUMS
runtime_log_group_nameDiagnose sanitized bootstrap and worker status
public_subnet_idConfirm the worker’s public-subnet placement
worker_security_group_idConfirm TCP/22 is restricted to the current jumpbox /32
target_urlVerify the exact authorized browser target
source_commitVerify immutable runtime provenance
termination_protection_enabledConfirm the destroy safeguard remains enabled

Use only the AWS outputs from terraform/aws; do not substitute Azure state or output values.

The browser receipt records immediate execution evidence: run, scenario, and step timestamps; assertion checks; terminal network states; screenshot capture status, relative path, intended object key, pending upload status, SHA-256, and masked-input count; CSD sensor and __imp_apg__/api/dip request counts; and cleanup status. The worker creates SHA256SUMS before upload. Upload success is represented by the Systems Manager command result, not by a rewritten receipt field. A separate read-only F5 Distributed Cloud observation may correlate telemetry by protected host and run window.

That correlation is not proof of causation. AWS datacenter reputation, browser automation signals, population thresholds, aggregation, and filtering can prevent or delay detection. Report only the observed status—OBSERVED, NOT_OBSERVED, PENDING, or ERROR—and never promise a detection, fixed delay, or risk classification.

Perform correlation only after retrieving and validating the exact committed S3 prefix. The authoritative upload-commit.json must exist at that prefix, name that exact prefix in its objectKey, record status: committed, and contain SHA-256 digests that match the retrieved upload-manifest.json and SHA256SUMS. Then validate SHA256SUMS, read receipt.json, and record runId, target.host, startedAt, and completedAt. Build the host comparison allowlist only from that receipt: the protected target.host and hosts in sanitized browser network outcomes that were actually observed. Use this exact host allowlist only for detected_domains and scripts. The receipt stores no generated form-field identifiers, so formFields is an optional aggregate observation for the receipt time window only. Do not attribute its records to a generated field or infer a field name from a scenario name or synthetic value.

Retrieve one exact run/scenario prefix and fail closed before correlation:

Terminal window
set -euo pipefail
export AWS_PROFILE=280469140135_Users
export AWS_REGION=us-east-1
EVIDENCE_BUCKET='<terraform-output-evidence_bucket_name>'
RUN_ID='<receipt-run-id>'
SCENARIO='<allowlisted-scenario>'
PREFIX="runs/${RUN_ID}/${SCENARIO}"
WORK_DIR=$(mktemp -d)
trap 'rm -rf "${WORK_DIR}"' EXIT
aws s3 cp "s3://${EVIDENCE_BUCKET}/${PREFIX}/upload-commit.json" "${WORK_DIR}/upload-commit.json"
jq -e --arg object_key "${PREFIX}/upload-commit.json" \
'.status == "committed" and .objectKey == $object_key' \
"${WORK_DIR}/upload-commit.json" >/dev/null
aws s3 cp "s3://${EVIDENCE_BUCKET}/${PREFIX}/" "${WORK_DIR}/" --recursive
jq -e --arg object_key "${PREFIX}/upload-commit.json" \
'.status == "committed" and .objectKey == $object_key' \
"${WORK_DIR}/upload-commit.json" >/dev/null
test "$(shasum -a 256 "${WORK_DIR}/upload-manifest.json" | awk '{print $1}')" = \
"$(jq -r '.uploadManifest.sha256' "${WORK_DIR}/upload-commit.json")"
test "$(shasum -a 256 "${WORK_DIR}/SHA256SUMS" | awk '{print $1}')" = \
"$(jq -r '.checksumSet.sha256' "${WORK_DIR}/upload-commit.json")"
(
cd "${WORK_DIR}"
shasum -a 256 -c SHA256SUMS
)
START_TIME=$(jq -r '.startedAt | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601' "${WORK_DIR}/receipt.json")
END_TIME=$(jq -r '.completedAt | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601' "${WORK_DIR}/receipt.json")
test "${START_TIME}" -le "${END_TIME}"

Run the following requests from an authenticated operator context with the supported xcsh_api tool. These blocks describe tool inputs; they are not shell commands. Replace the placeholders with the validated receipt values, epoch-second variables, and intended F5 Distributed Cloud namespace.

method: GET
path: /api/shape/csd/namespaces/{namespace}/detected_domains
params:
namespace: <namespace>
method: POST
path: /api/shape/csd/namespaces/{namespace}/scripts
params:
namespace: <namespace>
payload:
start_time: <START_TIME>
end_time: <END_TIME>
method: GET
path: /api/shape/csd/namespaces/{namespace}/formFields
params:
namespace: <namespace>
start_time: <START_TIME>
end_time: <END_TIME>

Response schemas can differ across these endpoints. Treat arrays nested beneath response objects as candidate records. For detected_domains and scripts, count evidence only when a candidate’s normalized host/domain exactly matches the receipt-derived host allowlist. For formFields, record only the aggregate record count and endpoint classification for the receipt time window; never record or claim a match to a specific generated field identifier. Preserve the raw operator response in the separately controlled correlation record; do not copy it into or rewrite receipt.json.

Normalize one status for each endpoint:

StatusDeterministic meaning
OBSERVEDdetected_domains or scripts succeeded and returned at least one exact receipt-derived host match; or formFields succeeded and returned a non-zero aggregate count for the receipt window.
NOT_OBSERVEDdetected_domains or scripts succeeded, no reviewed host matched, and the operator explicitly closed the observation review as final. This is absence in the queried response, not proof that detection cannot occur later. Do not use this status for formFields.
PENDINGdetected_domains or scripts succeeded with no host match while review remains open or processing is incomplete; or formFields succeeded with a zero aggregate count or incomplete processing. No fixed waiting period is implied.
ERRORThe request failed, returned a non-success status, or could not be interpreted without guessing its schema.

For a successful formFields response, classification is therefore only OBSERVED or PENDING; transport or uninterpretable-schema failures remain ERROR.

Record a separate correlation table; one row per endpoint and observation attempt:

Run IDReceipt window (UTC)EndpointReviewed comparison keysStatusObserved resultObservation time (UTC)Error/notes
<runId><startedAt>–<completedAt>detected_domains or scriptsExact receipt-derived hostsOBSERVED / NOT_OBSERVED / PENDING / ERRORReturned non-secret host/domain identifiers onlyOperator query timeSchema/transport detail without credentials
<runId><startedAt>–<completedAt>formFieldsAggregate receipt-window observation onlyOBSERVED / PENDING / ERRORCount and endpoint classification onlyOperator query timeSchema/transport detail without credentials

Complete the checklist deterministically:

  1. Retrieve upload-commit.json from the exact run/scenario prefix and require status: committed plus its exact expected objectKey.
  2. Validate its upload-manifest.json and SHA256SUMS digests, then validate every object in SHA256SUMS and freeze the browser receipt as execution evidence.
  3. Convert the receipt UTC timestamps to epoch seconds and extract the reviewed host allowlist from the receipt only.
  4. Submit all three operator-side requests with the exact namespace and epoch-second receipt window where specified.
  5. Compare exact normalized hosts for detected_domains and scripts; ignore all unrelated records. For formFields, record only aggregate count and classification without field attribution.
  6. Assign statuses using the deterministic endpoint-specific rules above and preserve errors rather than converting them to absence.
  7. Store the correlation table separately. Never change the browser scenario status, counts, receipt, or checksum set based on tenant telemetry.

The Azure deployment continues to target an F5 Distributed Cloud HTTP load balancer through TARGET_FQDN; TARGET_ORIGIN_IP remains an optional direct-origin baseline.

Azure Traffic Generator -> F5 XC HTTP LB -> Azure Origin Server
| |
+-> SSH runner +-> WAF / Bot / API / CSD policies

The origin server supplies the application paths used by the existing Azure suites, including Juice Shop, DVWA, VAmPI, crAPI, DVGA, Restaurant API, and the CSD demo. Configure the Azure runner in /opt/traffic-generator/config.env:

Terminal window
TARGET_FQDN=demo.example.com
TARGET_ORIGIN_IP=20.10.5.100

Run the existing Azure suites over SSH as documented in Runner. Direct-origin comparison is optional and valid only when the origin owner has authorized it. A script exit count is execution evidence, not proof that an attack succeeded or a security control detected it; verify application and F5 Distributed Cloud telemetry independently.

  1. Deploy the chosen origin and verify application health.
  2. Configure the F5 Distributed Cloud origin pool and HTTP load balancer.
  3. Enable and verify the security capability required by the selected demo.
  4. Deploy one traffic-generator root—Azure or AWS—with its own state.
  5. Verify the runner access paths: source-restricted SSH and Systems Manager recovery for AWS, or SSH for Azure.
  6. Execute csd-violations on AWS, or an applicable shell suite on Azure, and preserve its evidence outside Git.
  7. Correlate platform telemetry only after browser or shell execution is proven.

The origin, F5 Distributed Cloud configuration, Azure traffic generator, and AWS traffic generator may have separate owners and state. Teardown one component only from its owning configuration; never use one state to delete another component’s resources.