Map AWS Foundational Security controls to CDK/TypeScript unit tests
Learn how to map AWS Security Hub controls into CDK TypeScript tests with cfn-nag and policy-as-code patterns.
Security Hub gives you an excellent detection layer, but detection alone does not prevent regressions. If your team wants infrastructure as code that is secure by default, you need the same controls translated into repeatable checks at commit time. That is where AWS Foundational Security, CDK, TypeScript, cfn-nag, and unit tests form a practical policy-as-code stack that catches misconfigurations before deployment. Think of it like building guardrails for your cloud layout, similar to how teams protect release workflows with review discipline and automation in other domains, not unlike the structure-first thinking behind infrastructure choices that protect page ranking or the trust-oriented mindset in verification tools shaping the trust economy.
This guide shows you how to translate Security Hub controls into concrete assertions against synthesized CloudFormation. You will learn where CDK unit tests are strongest, where cfn-nag is the better backstop, and how to avoid the common trap of writing tests that merely mirror the code rather than assert the security intent. We will also cover how to organize controls, handle service-specific exceptions, and create a repeatable workflow your platform team can scale across stacks and repos. If you have ever tried to standardize enforcement across a growing system, the challenge will feel familiar to anyone who has had to vet integrations carefully before trusting them in production.
1. Why translate Security Hub controls into CDK tests?
Detection is not prevention
AWS Security Hub’s AWS Foundational Security Best Practices standard continuously evaluates accounts and workloads against a curated set of controls. That is extremely valuable for visibility, auditability, and centralized reporting, but it still leaves a gap between “we detected a problem” and “we prevented the problem from shipping.” CDK unit tests close that gap by validating the infrastructure definition before it becomes live cloud state. In practice, this means a developer sees a failing test in local CI instead of a finding after deployment.
This approach is especially useful for controls that are entirely deterministic from infrastructure code, such as encryption settings, public access flags, logging configuration, or metadata service requirements. It is less useful for controls that depend on runtime state, organizational contact details, or resources managed outside the stack. A strong program separates controls into “testable at synth time,” “testable via template linting,” and “detect only in Security Hub.” That classification keeps your team from overpromising what infrastructure tests can do.
Policy-as-code reduces drift
Manual reviews are inconsistent, even with talented reviewers. People miss subtle issues when changes are large, rushed, or repeated across many stacks. Policy-as-code turns the desired security posture into a durable artifact that can be run on every pull request. It is the same basic advantage you get from structured playbooks in other high-change environments, like the discipline behind growth tactics that respect the law or the operational clarity in operate-or-orchestrate portfolio decisions.
For infrastructure teams, drift often begins with convenience. A developer disables a log setting to unblock a deployment, or a module is copied from an older stack that predates the latest security requirements. Once the insecure pattern is accepted once, it tends to spread. Tests transform security from a tribal-memory problem into an enforced contract.
Security review becomes faster, not slower
Good security tests do not replace review; they improve it. Reviewers can focus on exceptions, architectural tradeoffs, and threat modeling instead of checking every boolean property in a large template. That is a much better use of senior engineering time. It also makes audits easier because you can show both the rule definition and evidence that the rule is enforced continuously through CI.
2. Understand the AWS Foundational Security controls model
Controls are mostly outcome-based
The AWS Foundational Security Best Practices standard groups controls across many services, including account hygiene, networking, logging, encryption, identity, and service-specific hardening. The important thing is that most controls describe an outcome rather than a specific implementation. For example, a control may require encryption at rest, HTTPS-only access, or IMDSv2 enforcement. That outcome-based shape is what makes translation into unit tests feasible. You are not checking whether a particular service API was called; you are checking whether the synthesized infrastructure expresses the required security property.
Source material from the AWS documentation shows the breadth of the standard, including controls like account security contact information, API Gateway logging and WAF association, ECS task security requirements, and Auto Scaling IMDSv2 enforcement. This breadth matters because it means your test framework needs a pattern library, not one-off assertions. A disciplined mapping approach saves you from rewriting logic for every new stack. It is similar in spirit to building reusable patterns rather than isolated tactics, like the scalable system thinking in enterprise training paths.
Some controls are account-level, some are stack-level
Not every Security Hub control belongs in a CDK unit test. Account-level controls such as security contact information are better handled by AWS Organizations automation, config rules, or control tower guardrails. Stack-level controls are where CDK shines: S3 bucket encryption, CloudTrail logging, KMS key policies, ECS task definitions, Lambda environment encryption, and security group restrictions. This distinction prevents false confidence.
A good rule of thumb is simple: if the desired state can be inferred from synthesized CloudFormation, you can likely test it in CDK. If the state lives outside the template or depends on runtime behavior, you should enforce it elsewhere and let Security Hub validate it after deployment. The strongest programs combine both, rather than trying to force one tool to do everything.
3. Build a translation framework for controls
Step 1: classify the control
Start by organizing each AWS Foundational Security control into a taxonomy. At minimum, classify each as template-detectable, lint-detectable, runtime-detectable, or manual exception. Template-detectable controls are perfect for CDK assertions. Lint-detectable controls are often better covered by cfn-nag. Runtime-detectable controls should usually remain in Security Hub, AWS Config, or service-native monitoring. Manual exceptions are rare and should be explicitly documented and time-boxed.
This classification step is the difference between a robust policy system and a pile of brittle tests. If you skip it, you will end up with duplicated checks or gaps that nobody owns. Teams that handle this well usually maintain a control matrix in version control and review it with security engineering. That matrix becomes a living contract, much like the discipline needed when building a secure integration strategy in vendor-locked API environments.
Step 2: define the assertion shape
Every control should map to one of a handful of assertion shapes: property equals expected value, property is absent, property matches regex, at least one resource exists, resource does not exist, or resource configuration has a required sub-property. In CDK, these become hasResourceProperties, absent, resourceCountIs, and related assertions from the assertions library. cfn-nag complements those checks by evaluating known anti-patterns across the synthesized template.
For example, “S3 buckets should enforce encryption at rest” becomes a property assertion on BucketEncryption. “Security groups should not allow unrestricted ingress” becomes either a template assertion for known ports or a cfn-nag rule that catches 0.0.0.0/0. “ECS tasks should not run with privileged containers” becomes an assertion against task definition properties. The trick is to standardize the mapping language so your whole team speaks the same enforcement dialect.
Step 3: codify exceptions and ownership
Security tests fail when exceptions are ad hoc. Every exception should have an owner, a reason, a date, and a compensating control. That way, when a test fails or a control cannot be asserted directly, the team has a paper trail instead of folklore. In practical terms, this may mean an allowlist file, a tagged CDK construct, or a documented suppression in cfn-nag with justification. If you treat exceptions casually, the program turns into theater.
Pro Tip: Build your control matrix so every row answers four questions: What is the control? Can CDK assert it? Can cfn-nag lint it? What is the fallback detection path in Security Hub? That simple table prevents blind spots.
4. Implement CDK unit tests that assert security intent
Test the synthesized template, not the construct source
CDK unit tests should validate the generated CloudFormation template because that is the contract AWS actually consumes. Testing TypeScript implementation details is fragile and provides less value. The @aws-cdk/assertions library lets you synthesize a stack and make assertions about resources, properties, counts, and absence. This is where your security policy becomes code that can fail a build. It is as repeatable as any well-structured automation workflow, similar to how teams standardize coverage in repeatable demo workflows.
For example, if a control requires encryption on an S3 bucket, your test should assert that the bucket uses a KMS-managed key or SSE-S3 depending on your policy. If a control requires HTTPS, assert that listeners and policies redirect or reject plaintext traffic. If a control requires logging, assert that the service-specific log destination, retention, and permissions exist. This keeps security requirements visible and reviewable in the same repository as the infrastructure.
Example: S3 bucket encryption
import { App, Stack } from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
import { Bucket } from 'aws-cdk-lib/aws-s3';
test('S3 buckets are encrypted at rest', () => {
const app = new App();
const stack = new Stack(app, 'TestStack');
new Bucket(stack, 'DataBucket', {
encryption: BucketEncryption.KMS,
enforceSSL: true,
versioned: true,
});
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::S3::Bucket', {
BucketEncryption: {
ServerSideEncryptionConfiguration: Match.arrayWith([
Match.objectLike({
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'aws:kms',
},
}),
]),
},
});
});This is more than a feature test. It is a security assertion. The value is not that the bucket exists, but that the bucket meets the encryption requirement corresponding to a foundational control. In practice, you should wrap this in helper functions so teams can apply the same pattern to many resource types without copy-paste noise.
Example: enforcing IMDSv2 for EC2 and Auto Scaling
One of the clearest AWS Foundational Security patterns is IMDSv2 enforcement for EC2 instances launched via Auto Scaling. In CDK, you can assert that the launch template or machine image settings require HttpTokens: required. This is an excellent unit-test candidate because the desired outcome is explicit and stable. If your stack creates launch configurations or templates, verify the metadata options in the synthesized template rather than assuming the construct defaults are sufficient.
When mapping controls like this, pay attention to the construct abstraction. Some higher-level CDK constructs expose security defaults, while others delegate to lower-level CloudFormation properties. If you are not careful, a later refactor can remove a safe default without any visible compile-time signal. That is one reason teams investing in TypeScript security tests often pair them with architecture reviews and reuse guardrails, not unlike the strategic planning mindset in workforce planning.
Use negative assertions aggressively
Many security defects are about prohibited states, not just required properties. You should assert that public IPs are not assigned, broad CIDRs are absent, access keys are not hardcoded, and anonymous access is blocked. Negative assertions are powerful because they prevent accidental regressions when teams extend a stack. They also tend to surface issues that “happy path” tests miss.
For example, if a control says an RDS database should not be publicly accessible, assert that the resource does not set PubliclyAccessible: true. If a control says an API must use an authorizer, assert that every route includes an authorization configuration. If a control says CloudWatch Logs retention should be set, assert that the log group has an explicit retention period rather than relying on the default. These are the kinds of checks that make infrastructure truly compliance-aware.
5. Where cfn-nag fits better than unit tests
Use cfn-nag for broad anti-pattern detection
cfn-nag is especially useful when you want detection of known risky patterns across the synthesized template without writing a bespoke assertion for each construct. It is strong for broad issues like open security groups, permissive IAM policies, wildcard principals, public S3 ACLs, or missing encryption settings. In other words, cfn-nag is your linting safety net, while CDK unit tests are your intentional policy checks. The combination is stronger than either one alone.
This matters because CDK tests can become overly specific to your construct implementation. cfn-nag helps catch security debt that slips through because of reused modules, raw CloudFormation snippets, or edge-case resource types. Its rule set is also a good educational tool for developers who are still learning where cloud security defaults are dangerous. Think of it as a structural backstop, the way high-quality review systems keep teams honest in fast-moving environments like rapid comparison publishing or deliverability-focused systems.
Use suppressions carefully
Every security tool needs an exception mechanism, and cfn-nag is no different. But suppressions should never become a habit. If a suppression is necessary, make it narrow, documented, and reviewable. Better still, tie it to a ticket or risk acceptance record with an expiration. If your suppression count grows without governance, the tool stops representing the real security posture.
A good operational pattern is to require developers to explain why a finding is safe in context, what compensating control exists, and when the suppression should be revisited. This mirrors mature risk management in other technical fields: the point is not to ban tradeoffs, but to make them explicit and traceable. That kind of discipline is what separates reliable policy-as-code from checkbox compliance.
Let cfn-nag cover the long tail
You do not need to write a CDK assertion for every possible insecure property. For example, if multiple controls around IAM, S3, and network exposure are already well-covered by cfn-nag, let the linter handle the broad anti-pattern set. Then reserve your custom unit tests for the critical organizational controls that your platform truly depends on. This division keeps your test suite maintainable and avoids redundant assertions.
6. A practical control-to-test mapping table
The following table shows how common AWS Foundational Security controls can be translated into enforcement patterns. The goal is not to mirror every Security Hub control one-to-one, but to use a repeatable mapping strategy that can scale across services. If your team standardizes this table in a repo, it becomes the source of truth for developers and security reviewers alike.
| Security Hub control theme | Example control | Best enforcement layer | CDK assertion pattern | cfn-nag value |
|---|---|---|---|---|
| Encryption at rest | S3, EBS, RDS, EFS encryption enabled | CDK unit test + cfn-nag | hasResourceProperties on encryption config | Detect missing encryption across resources |
| Public access prevention | S3 buckets not public, no 0.0.0.0/0 ingress | CDK unit test + cfn-nag | Assert absence of public ACLs or wide CIDRs | Catch broad network exposure patterns |
| Logging enabled | API Gateway access/execution logs, CloudTrail, ELB logs | CDK unit test | Assert log group, stage logging, retention | Spot missing or misconfigured log resources |
| Secure transport | HTTPS-only access, TLS for private integrations | CDK unit test | Assert redirect policies or TLS config | Catch plaintext endpoint exposure |
| Privilege minimization | IAM policies avoid wildcards; no privileged containers | CDK unit test + cfn-nag | Assert policy scope and container flags | Identify wildcard actions/resources |
| Metadata hardening | EC2/ASG require IMDSv2 | CDK unit test | Assert HttpTokens: required | Helpful as an extra check |
| Authorization required | API Gateway routes specify auth type | CDK unit test | Assert each route has authorizer/authorization | Limited value; template assertion is clearer |
7. Patterns by AWS service: how to translate the most common controls
Amazon S3, RDS, and EBS
S3, RDS, and EBS are among the easiest services to test because their security posture is expressed through explicit template properties. For S3, assert encryption, public access block configuration, SSL enforcement, versioning where required, and lifecycle settings when your policy mandates them. For RDS, assert storage encryption, no public accessibility, and logging options. For EBS, assert encrypted volumes and key usage where your standards require customer-managed keys.
These checks map very cleanly to CDK tests and often need only minimal cfn-nag support. The main risk is implementation drift: a developer swaps a secure construct for a lower-level resource or changes a default parameter. When you encode these properties in tests, you make the secure choice the easiest choice. That is the essence of security-as-code.
API Gateway, Lambda, and AppSync
API-facing services demand extra attention because security failures often appear as missing auth, logging, or transport restrictions. For API Gateway, translate controls into assertions for access logs, execution logs, X-Ray where appropriate, WAF associations, and authorization types for routes. For Lambda, enforce KMS-encrypted environment variables, least-privilege execution roles, and log retention. For AppSync, ensure field-level logging and reject API-key-only authentication where your policy disallows it.
This is also where your team should keep a careful eye on service abstractions. Higher-level constructs may hide the exact CloudFormation resource, so your test helpers should inspect the synthesized output rather than assumptions about the construct API. If you need a working mental model for handling platform constraints without losing architectural control, the thinking aligns with pieces like offline reliability patterns and integration-boundary design.
ECS, EKS, and compute hardening
For container workloads, the foundational controls commonly revolve around logging, task metadata exposure, network placement, and image or runtime configuration. In CDK, test for readonly root filesystems where required, execution roles instead of broad instance roles, no privileged containers, and logging drivers or destinations. On EKS, some controls are better enforced with admission policy, managed add-ons, or cluster-level guardrails, but you can still unit-test the infrastructure that creates the cluster, node groups, and security groups.
Containerized systems are especially prone to drift because a single team may own the base chart while multiple service teams deploy from it. That makes template-level security tests valuable even when runtime policy engines exist. The goal is to make secure defaults hard to bypass. The best teams treat infrastructure tests as a product, not a one-off compliance task.
8. A recommended implementation blueprint for TypeScript CDK repos
Folder structure and helper libraries
Organize your security checks into reusable helper modules rather than scattering assertions across every stack test. A practical structure is to create a security-tests folder with shared assertion helpers like assertS3Encrypted, assertNoPublicIngress, and assertLogRetention. This reduces duplication and makes changes easier when AWS service behavior evolves. It also helps new contributors learn the house style for security verification.
You can then pair those helpers with stack-specific tests for exceptions or unique architecture decisions. This layered design makes your tests easier to read and safer to modify. If a company later adds more teams or more accounts, the helpers become a governance accelerator rather than a maintenance burden. That same scalable mindset shows up in successful operational systems across industries, like the structured decision-making in competitive IP analysis and invalid.
Pipeline stages that should fail fast
Your CI pipeline should generally run three security gates in sequence: TypeScript compile/lint, CDK unit tests, and cfn-nag on synthesized templates. The earlier a failure is caught, the cheaper it is to fix. If the compile step fails, developers know the code is syntactically wrong. If a unit test fails, they know a security invariant changed. If cfn-nag fails, they know the template contains a recognized anti-pattern worth reviewing.
In mature pipelines, these checks run before any deployment stage. That reduces the chance that a temporary misconfiguration makes it into a test or prod environment. It also creates a clear audit trail of what was checked and when. If your organization already uses release orchestration or approval workflows, this fits naturally into that model, much like coordinating operational decisions in operate-or-orchestrate frameworks.
Handling false positives and policy exceptions
No security automation is perfect. A good system expects a small number of false positives and treats them as engineering work, not justification to disable enforcement. When you encounter a legitimate false positive, update the shared helper or suppression process so it is handled consistently. Avoid one-off edits in individual repositories, which create invisible debt.
A useful governance pattern is to require exception reviews monthly or quarterly. That gives teams enough runway to ship while still forcing risk owners to revisit old decisions. In practice, most repeated exceptions reveal either a missing helper, a bad abstraction, or a control that should move to another layer such as AWS Config or Security Hub. A mature program evolves the boundary rather than just tolerating noise.
9. A sample workflow for a single control from doc to test
Start with the source control statement
Suppose Security Hub says that API Gateway REST API stages should have access logging enabled. The first step is to define the control in your own words, including what “enabled” means in your environment. Does it require a specific destination log group, a minimum retention period, redaction, or structured logs? The more explicit your language, the less room there is for interpretation later.
Then map that statement to the template property or properties you need to assert. In this case, you might check that a stage has access logging configured and that the destination ARN is present. If your policy also requires retention, you should assert the log group configuration separately. That is how one broad control becomes several precise tests.
Encode the helper and the test
Create a helper function such as assertApiGatewayAccessLoggingEnabled(template, stageLogicalId). Inside the helper, check for the expected stage configuration and any associated log group resources. Keep the helper small and opinionated. This is where teams often overcomplicate things, but simple helpers are easier to reuse and easier to debug.
Then write the stack test using the helper. The test should not re-derive the policy. It should describe the intended outcome and call the shared assertion. That separation makes the policy centralized and the stack test readable. It also means a future update to the control only requires changing one helper rather than dozens of tests.
Backstop with cfn-nag and Security Hub
Once the unit test passes, run cfn-nag as a second line of defense and let Security Hub remain the production-time monitoring layer. If the stack somehow includes an unintended exposure that your helper missed, cfn-nag may still catch it. If something only manifests after deployment or through account-wide context, Security Hub can identify it continuously. This layered design is what makes the program resilient instead of merely symbolic.
Pro Tip: Treat CDK unit tests as “required secure intent,” cfn-nag as “known bad patterns,” and Security Hub as “continuous environment truth.” When each layer has a distinct job, the whole system is easier to trust.
10. Common mistakes to avoid
Testing the wrong abstraction layer
One of the biggest mistakes is asserting too much about construct internals and too little about the final template. If your test only checks that a helper was called or that a class property exists, it may pass while the synthesized infrastructure is still unsafe. The reverse problem is also common: overfitting tests to exact CloudFormation shapes makes refactors painful. Aim for intent-level checks that are stable across reasonable implementation changes.
Duplicating Security Hub without adding value
If your CDK tests simply restate Security Hub checks without accounting for your architecture, you are duplicating effort instead of reducing risk. The real value comes from tightening the feedback loop and encoding organization-specific defaults. That might mean mandatory KMS keys, stricter log retention, or service-specific deny rules. Use the AWS standard as the baseline, then harden it to your needs.
Leaving exceptions undocumented
Undocumented exceptions are one of the fastest ways to erode trust in a policy program. They also make audits harder because nobody can explain why a control was bypassed. Every exception should be visible in code review, and ideally it should be easy to search across repos. When exceptions are too easy to create and too hard to inspect, the security posture becomes a guessing game.
11. FAQ
How do I decide whether a control belongs in CDK tests or Security Hub?
Use CDK tests when the control can be determined from the synthesized infrastructure template. Use Security Hub when the control depends on deployed state, account-level configuration, or behavior outside the stack. Many teams use both: CDK to prevent regressions and Security Hub to continuously monitor the live environment. If a control is service- or runtime-dependent, do not force it into a unit test just to say you covered it.
Can cfn-nag replace custom unit tests?
Not really. cfn-nag is excellent for broad anti-pattern detection, but it does not understand your organization’s exact policy decisions or architectural exceptions. Custom unit tests are better for deterministic, high-value requirements like encryption, logging, and IMDSv2. The strongest program uses both, with unit tests encoding intent and cfn-nag catching known bad patterns.
What if a CDK construct hides the CloudFormation property I need to check?
Inspect the synthesized template, not the higher-level construct API. If necessary, create a small helper that converts the construct into a template assertion target. Some constructs expose safe defaults directly; others require explicit configuration or low-level overrides. Your test should always validate the final security outcome, regardless of abstraction level.
How do I handle controls that are hard to test directly, like account-level security contact info?
Move those controls to the right layer. Account-level requirements are usually better handled by AWS Organizations, landing zone automation, AWS Config, or manual governance workflows with Security Hub validation. CDK unit tests are best for stack-scoped infrastructure. Trying to force everything into the same mechanism usually creates brittle automation and false confidence.
Should we fail builds on all cfn-nag findings?
For production-bound infrastructure, yes, unless there is a documented and approved suppression process. For experimental or temporary stacks, you may choose a softer path, but that should be the exception. The key is consistency: the same class of finding should be treated the same way across teams unless there is a deliberate policy difference.
How do we scale this across many repositories?
Create shared security assertion libraries, standard control mappings, and a published exception process. Then make the CI pipeline template reusable so every repo gets the same baseline checks. Centralizing helpers while allowing stack-specific tests is usually the sweet spot. That gives you governance without freezing developers into a rigid, unmaintainable framework.
12. Conclusion: turn compliance into engineering feedback
AWS Foundational Security controls are most valuable when they become part of your development workflow, not just a dashboard after deployment. By translating the controls into CDK/TypeScript unit tests, augmenting them with cfn-nag, and preserving Security Hub as continuous detection, you create a layered defense that is practical and auditable. The result is faster feedback, fewer regressions, and a clearer security contract between platform teams and application developers.
If your organization is serious about policy-as-code, start with a small set of high-signal controls: encryption, public exposure, logging, transport security, IAM wildcards, and IMDSv2. Build shared helpers, document exceptions, and make the tests easy to run locally and in CI. Over time, you can expand the library into a comprehensive compliance framework that grows with your infrastructure rather than lagging behind it. That is how security becomes a feature of your delivery system, not a tax on it.
To keep expanding your TypeScript security toolchain, it also helps to understand how teams operationalize related systems such as infrastructure strategy, edge reliability patterns, and observability-driven workflows. The underlying lesson is consistent: the more you can encode into automated checks, the less you depend on memory, heroics, or hope.
Related Reading
- How to Build Around Vendor-Locked APIs - Useful for designing secure abstractions around constrained platform APIs.
- Vet Your Partners with GitHub Activity - A good lens for evaluating dependencies and integrations before adoption.
- Operate or Orchestrate? - Helpful for deciding where policy should be centralized versus delegated.
- Infrastructure Choices That Protect Page Ranking - A strong analogy for systems that depend on durable guardrails.
- Rethinking App Infrastructure - Relevant for teams formalizing infrastructure strategy and control layers.
Related Topics
Avery Morgan
Senior SEO Content Strategist
Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.
Up Next
More stories handpicked for you