mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-03 01:03:26 +00:00
PR#1 of the AWS migration. CDK TypeScript app under /infra: stacks open-swe-iam (four per-env GitHub-OIDC deploy roles) + open-swe-dev/-prod (per-env EC2 instance role + AMI cache). aws-cdk-lib pinned exact 2.260.0; kebab naming Aspect + 15 tests. IAM is synth-only (NOT deployed). Cleared the Phase-1 security gates: - T4 GPT-4.1 IAM cross-review (StringEquals trust; cdk-hnb659fds-* wildcard kept as org convention; AWS-RunShellScript timeboxed to T19). - T5 /sh-security-review: deploy roles split PER-ENV with env-scoped OIDC trust (dev=branch ref+tag dev, prod=environment:prod+tag prod) so a dev token cannot reach prod; re-verified block:false.
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
import { Annotations, CfnResource, IAspect, Stack, Token } from "aws-cdk-lib";
|
|
import { IConstruct } from "constructs";
|
|
|
|
/**
|
|
* One "/"-delimited segment must be lower kebab-case: `a-b-c`, digits allowed.
|
|
*/
|
|
const KEBAB_SEGMENT = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
|
|
/**
|
|
* CloudFormation property keys that carry an *explicit physical name*. The
|
|
* codegen'd L1 stores these either camelCased (`roleName`) or CFN-cased
|
|
* (`RoleName`) depending on the construct, so the aspect matches keys
|
|
* case-insensitively.
|
|
*
|
|
* We deliberately validate physical NAMES + the stack name only — not CDK
|
|
* logical construct ids (those are conventionally PascalCase, e.g.
|
|
* `InfraDeployRole`, and validating them would be wrong).
|
|
*/
|
|
const NAME_PROPERTY_KEYS = [
|
|
"RoleName",
|
|
"BucketName",
|
|
"FunctionName",
|
|
"TableName",
|
|
"LogGroupName",
|
|
"QueueName",
|
|
"TopicName",
|
|
"SecretName",
|
|
"StreamName",
|
|
"RepositoryName",
|
|
"DBInstanceIdentifier",
|
|
"DBClusterIdentifier",
|
|
"StateMachineName",
|
|
"RuleName",
|
|
"UserPoolName",
|
|
];
|
|
// NOTE: `PolicyName` is intentionally NOT checked — CDK auto-generates inline
|
|
// `DefaultPolicy` names (e.g. "InstanceRoleDefaultPolicyF15F...") from the
|
|
// logical id; those are not explicit, user-controlled physical names and are
|
|
// outside the naming convention's scope.
|
|
|
|
const NAME_KEYS_LC = new Set(NAME_PROPERTY_KEYS.map((k) => k.toLowerCase()));
|
|
|
|
/**
|
|
* `true` when every non-empty "/"-delimited segment is kebab-case.
|
|
*
|
|
* Path-style names are tolerated so the same check works for Secrets Manager
|
|
* (`open-swe-dev/foo`), SSM params (`/open-swe-dev/foo`) and log groups
|
|
* (`/open-swe/dev/agent`): each segment is validated independently, and a
|
|
* leading slash (empty first segment) is ignored.
|
|
*/
|
|
export function isKebabCase(value: string): boolean {
|
|
return value
|
|
.split("/")
|
|
.filter((seg) => seg.length > 0)
|
|
.every((seg) => KEBAB_SEGMENT.test(seg));
|
|
}
|
|
|
|
/**
|
|
* Aspect that FAILS synth (`Annotations.addError`) when an explicitly-named
|
|
* resource — or a stack name — is not kebab-case. Enforces the org naming
|
|
* convention (naming-conventions.md) deterministically at synth time so a
|
|
* non-conforming name can never reach a deploy. Wired in bin/app.ts via
|
|
* `Aspects.of(app).add(new KebabNamingAspect())`.
|
|
*/
|
|
export class KebabNamingAspect implements IAspect {
|
|
public visit(node: IConstruct): void {
|
|
if (node instanceof Stack) {
|
|
const name = node.stackName;
|
|
if (!Token.isUnresolved(name) && !isKebabCase(name)) {
|
|
Annotations.of(node).addError(
|
|
`Stack name "${name}" is not kebab-case (open-swe naming convention).`,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (node instanceof CfnResource) {
|
|
// `_cfnProperties` is the props as set on the L1; resolve to collapse any
|
|
// intrinsic tokens (refs/getatt) so only literal strings are checked.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const raw = (node as any)._cfnProperties ?? {};
|
|
const resolved = Stack.of(node).resolve(raw) ?? {};
|
|
for (const [key, value] of Object.entries(resolved)) {
|
|
if (
|
|
NAME_KEYS_LC.has(key.toLowerCase()) &&
|
|
typeof value === "string" &&
|
|
!Token.isUnresolved(value) &&
|
|
!isKebabCase(value)
|
|
) {
|
|
Annotations.of(node).addError(
|
|
`Resource "${node.node.path}" property ${key}="${value}" is not kebab-case ` +
|
|
`(open-swe naming convention).`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|