diff --git a/CHANGELOG.md b/CHANGELOG.md index b5b7c83..5b224c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ +## Pending -## 0.5.0 - -- Added in checks for `alb` and `lb` to the compute and network policies -- add new securityGroupNoRuleManagementConflicts rule [#108](https://github.com/pulumi/pulumi-policy-aws/pull/108) +- Add checks for `alb` and `lb` to the compute and network policies [#903](https://github.com/pulumi/pulumi-policy-aws/pull/93) +- Update to latest `@pulumi/policy` [#103](https://github.com/pulumi/pulumi-policy-aws/pull/103) +- Add checks for iam.Role policy usage with aws.iam.RolePolicy [#107](https://github.com/pulumi/pulumi-policy-aws/pull/107) +- Add checks for SecurityGroup resource best practices [#108](https://github.com/pulumi/pulumi-policy-aws/pull/108) ## 0.4.0 (2022-09-22) diff --git a/integration-tests/iam/Pulumi.yaml b/integration-tests/iam/Pulumi.yaml new file mode 100644 index 0000000..5eaab0e --- /dev/null +++ b/integration-tests/iam/Pulumi.yaml @@ -0,0 +1,3 @@ +name: awsguard-test-compute +runtime: nodejs +description: Tests for policy rules related to AWS Compute. diff --git a/integration-tests/iam/index.ts b/integration-tests/iam/index.ts new file mode 100644 index 0000000..d65c997 --- /dev/null +++ b/integration-tests/iam/index.ts @@ -0,0 +1,123 @@ +// Copyright 2016-2024, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as aws from "@pulumi/aws"; +import * as pulumi from "@pulumi/pulumi"; + +const config = new pulumi.Config(); +const testScenario = config.getNumber("scenario"); + +export let result: pulumi.Output; + +console.log(`Running test scenario #${testScenario}`); + +switch (testScenario) { + case 1: // Role with managedPolicyArns by itself - OK + const role1 = new aws.iam.Role("role1", { + assumeRolePolicy: JSON.stringify({ + Version: "2012-10-17", + Statement: [{ + Action: "sts:AssumeRole", + Effect: "Allow", + Sid: "", + Principal: { + Service: "lambda.amazonaws.com", + }, + }], + }), + managedPolicyArns: [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + }); + + result = role1.arn; + break; + case 2: // Role with RolePolicyAttachment - OK + const role2 = new aws.iam.Role("role1", { + assumeRolePolicy: JSON.stringify({ + Version: "2012-10-17", + Statement: [{ + Action: "sts:AssumeRole", + Effect: "Allow", + Sid: "", + Principal: { + Service: "lambda.amazonaws.com", + }, + }], + }), + }); + + const policyDoc2 = aws.iam.getPolicyDocument({ + statements: [{ + effect: "Allow", + actions: ["ec2:Describe*"], + resources: ["*"], + }], + }); + + const policy2 = new aws.iam.Policy("policy", { + description: "A test policy", + policy: policyDoc2.then(policy => policy.json), + }); + + const roleAttach2 = new aws.iam.RolePolicyAttachment("rpa", { + role: role2.name, + policyArn: policy2.arn, + }); + + result = roleAttach2.urn; + + break; + case 3: // Role with managedPolicyArns conflicts with RolePolicyAttachment + const role3 = new aws.iam.Role("role1", { + assumeRolePolicy: JSON.stringify({ + Version: "2012-10-17", + Statement: [{ + Action: "sts:AssumeRole", + Effect: "Allow", + Sid: "", + Principal: { + Service: "lambda.amazonaws.com", + }, + }], + }), + managedPolicyArns: [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + }); + + const policyDoc3 = aws.iam.getPolicyDocument({ + statements: [{ + effect: "Allow", + actions: ["ec2:Describe*"], + resources: ["*"], + }], + }); + + const policy3 = new aws.iam.Policy("policy", { + description: "A test policy", + policy: policyDoc3.then(policy => policy.json), + }); + + const roleAttach3 = new aws.iam.RolePolicyAttachment("rpa", { + role: role3.name, + policyArn: policy3.arn, + }); + + result = roleAttach3.urn; + + break; + default: + throw new Error(`Unexpected test scenario ${testScenario}`); +} diff --git a/integration-tests/iam/package.json b/integration-tests/iam/package.json new file mode 100644 index 0000000..1fc6b6a --- /dev/null +++ b/integration-tests/iam/package.json @@ -0,0 +1,12 @@ +{ + "name": "awsguard-test-iam", + "main": "index.ts", + "dependencies": { + "@pulumi/pulumi": "^3.0.0", + "@pulumi/aws": "^6.0.0", + "@pulumi/awsx": "^2.9.0" + }, + "resolutions": { + "@pulumi/aws": "^6.0.0" + } +} diff --git a/integration-tests/integration_test.go b/integration-tests/integration_test.go index 8f52d65..e4da065 100644 --- a/integration-tests/integration_test.go +++ b/integration-tests/integration_test.go @@ -171,6 +171,24 @@ func TestElasticSearch(t *testing.T) { }) } +func TestIAM(t *testing.T) { + runPolicyPackIntegrationTest( + t, "iam", + awsGuardSettings{}, + map[string]string{ + "aws:region": "us-west-2", + }, + []policyTestScenario{ + // Test scenario 1 and 2 - happy path. + {}, {}, + // Test scenario 3 - managedPolicyArns conflict. + { + WantErrors: []string{"RolePolicyAttachment should not be used with a role"}, + }, + }, + ) +} + func TestComputeEC2(t *testing.T) { runPolicyPackIntegrationTest( t, "compute", diff --git a/src/iam.ts b/src/iam.ts new file mode 100644 index 0000000..e39fc41 --- /dev/null +++ b/src/iam.ts @@ -0,0 +1,86 @@ +// Copyright 2016-2024, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + EnforcementLevel, + ReportViolation, + StackValidationArgs, + StackValidationPolicy, +} from "@pulumi/policy"; + +import { registerPolicy } from "./awsGuard"; + +// Mixin additional properties onto AwsGuardArgs. +declare module "./awsGuard" { + interface AwsGuardArgs { + iamRoleNoPolicyManagementConflicts?: EnforcementLevel; + } +} + + +/** @internal */ +// Enforce the note on aws.iam.Role: +// +// NOTE: If you use this resource’s managed_policy_arns argument or inline_policy configuration blocks, this resource +// will take over exclusive management of the role's respective policy types (e.g., both policy types if both arguments +// are used). These arguments are incompatible with other ways of managing a role's policies, such as +// aws.iam.PolicyAttachment, aws.iam.RolePolicyAttachment, and aws.iam.RolePolicy. If you attempt to manage a role’s +// policies by multiple means, you will get resource cycling and/or errors. +export const iamRoleNoPolicyManagementConflicts: StackValidationPolicy = { + name: "iam-role-no-policy-management-conflicts", + description: "Checks that iam.Role resources do not conflict with iam.PolicyAttachment, iam.RolePolicyAttachment, iam.RolePolicy", + validateStack: (args: StackValidationArgs, reportViolation: ReportViolation) => { + args.resources.forEach(r => { + let roleProp: string; + let currentType: string; + switch (r.type) { + case "aws:iam/policyAttachment:PolicyAttachment": { + roleProp = "roles"; + currentType = "PolicyAttachment"; + break; + } + case "aws:iam/rolePolicyAttachment:RolePolicyAttachment": { + roleProp = "role"; + currentType = "RolePolicyAttachment"; + break; + } + case "aws:iam/rolePolicy:RolePolicy": { + roleProp = "role"; + currentType = "RolePolicy"; + break; + } + default: { + return; + } + } + + if (r.propertyDependencies[roleProp]) { + r.propertyDependencies[roleProp].forEach(dep => { + if (dep.type !== "aws:iam/role:Role" || !dep.props) { + return; + } + if (dep.props["managedPolicyArns"] && dep.props["managedPolicyArns"].length > 0) { + reportViolation(`${currentType} should not be used with a role ${dep.urn} that defines managedPolicyArns`, r.urn); + } + if (dep.props["inlinePolicies"] && dep.props["inlinePolicies"].length > 0) { + reportViolation(`${currentType} should not be used with a role ${dep.urn} that defines inlinePolicies ${JSON.stringify(dep.props["inlinePolicies"])}`, r.urn); + } + }); + } + return; + }); + }, +}; + +registerPolicy("iamRoleNoPolicyManagementConflicts", iamRoleNoPolicyManagementConflicts); diff --git a/src/index.ts b/src/index.ts index 12e614b..d069008 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ import "./apiGateway"; import "./compute"; import "./database"; import "./elasticsearch"; +import "./iam"; import "./network"; import "./security"; import "./storage"; diff --git a/src/tsconfig.json b/src/tsconfig.json index 9afdf33..ad2ebad 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -21,6 +21,7 @@ "database.ts", "elasticsearch.ts", "enforcementLevel.ts", + "iam.ts", "index.ts", "network.ts", "policyArgs.ts",