Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ clean: # Cleanup build artifacts

build: clean ## Build the plugin package
@mkdir -p dist/
@go build -o dist/plugin main.go
@go build -o dist/plugin .

run: build ## Execute the Concom agent with the built plugin
@../agent/dist/./concom agent --config ./.config/config.yaml
81 changes: 77 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,79 @@
# Plugin AWS EC2
# AWS EC2 CCF Plugin

## Assumptions
This plugin collects read-only AWS EC2, EBS, snapshot, and recovery metadata, evaluates CCF Rego policy bundles, and emits evidence back through the CCF agent.

- That the agent running this plugin has access to the classic AWS env vars in order to use SDK methods
- There is a set of policies specfically scoped to EC2 that the config in the agent is pointing to
## Supported resource families

The collector can evaluate policies for:

- EC2 instances
- attached security groups
- attached EBS volumes
- account-owned EBS snapshots for attached volumes
- snapshot restore permissions
- account-owned AMIs related to the instance or collected snapshots
- Fast Snapshot Restore state for collected snapshots

## How it fits in CCF

The CCF agent starts this binary through HashiCorp `go-plugin`, passes configuration and policy paths over gRPC, and receives generated evidence through the runner callback. This repository does not call the CCF API directly.

During `Init`, the plugin also registers EC2 subject templates and risk templates discovered from the supplied policy bundles.

## Default policy bundle mapping

| Repository | Behavior | Primary input |
| --- | --- | --- |
| `plugin-aws-ec2-policies` | `ec2` | `input.instance` plus related EC2, EBS, and recovery fields |

## Configuration

The plugin expects:

- AWS credentials through the default AWS SDK credential chain
- target regions from `config.regions`
- `AWS_REGION` env as a fallback when plugin config does not provide a region

`config.regions` can contain a comma-separated list. Duplicate and empty region values are ignored.

Any agent-supplied `policy_data` is passed through to Rego as `data.*`.

## Data collected

For each running, stopped, stopping, or starting EC2 instance in each configured region, the plugin can collect and correlate:

- instance details and metadata options
- VPC and subnet identifiers
- attached security groups
- attached EBS volumes
- account-owned snapshots for attached volumes
- derived snapshot inventory, including encryption and public restore flags
- snapshot create-volume permissions
- account-owned AMIs that match the instance image or collected snapshots
- Fast Snapshot Restore entries for collected snapshots

## Development

Run the local test suite with:

```shell
go test ./...
```

Or use the Makefile wrapper:

```shell
make test
```

Build the plugin binary with:

```shell
make build
```

This writes the compiled plugin to `dist/plugin`.

## Related repositories

- [plugin-aws-ec2-policies](https://github.com/compliance-framework/plugin-aws-ec2-policies)
3 changes: 2 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ func (l *CompliancePlugin) Configure(req *proto.ConfigureRequest) (*proto.Config
}

func (l *CompliancePlugin) Init(req *proto.InitRequest, apiHelper runner.ApiHelper) (*proto.InitResponse, error) {
return &proto.InitResponse{}, nil
ctx := context.Background()
return runner.InitWithSubjectsAndRisksFromPolicies(ctx, l.logger, req, apiHelper, buildSubjectTemplates())
}

func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.ApiHelper) (*proto.EvalResponse, error) {
Expand Down
45 changes: 45 additions & 0 deletions subject_templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import "github.com/compliance-framework/agent/runner/proto"

func buildSubjectTemplates() []*proto.SubjectTemplate {
return []*proto.SubjectTemplate{
{
Name: "aws-ec2-instance",
Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT,
TitleTemplate: `AWS EC2 instance {{ index . "instance-id" }} in {{ .region }}`,
DescriptionTemplate: `Amazon EC2 instance {{ index . "instance-id" }} in subnet {{ index . "_subnet-id" }} and VPC {{ index . "_vpc-id" }} within AWS region {{ .region }}.`,
PurposeTemplate: "Represents an AWS EC2 instance evaluated for compute, network exposure, and recovery posture.",
IdentityLabelKeys: []string{"provider", "region", "instance-id"},
SelectorLabels: selectorLabelsForType("ec2"),
LabelSchema: labelSchema(
label("provider", "Cloud provider for the evaluated resource"),
label("type", "EC2 plugin resource type"),
label("instance-id", "AWS EC2 instance identifier"),
label("_vpc-id", "AWS VPC identifier containing the instance"),
label("_subnet-id", "AWS subnet identifier containing the instance"),
label("region", "AWS region containing the instance"),
),
},
}
}

func selectorLabelsForType(resourceType string) []*proto.SubjectLabelSelector {
return []*proto.SubjectLabelSelector{
{
Key: "type",
Value: resourceType,
},
}
}

func label(key string, description string) *proto.SubjectLabelSchema {
return &proto.SubjectLabelSchema{
Key: key,
Description: description,
}
}

func labelSchema(labels ...*proto.SubjectLabelSchema) []*proto.SubjectLabelSchema {
return labels
}
90 changes: 90 additions & 0 deletions subject_templates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"bytes"
"testing"
"text/template"

"github.com/compliance-framework/agent/runner/proto"
)

func TestBuildSubjectTemplates(t *testing.T) {
templates := buildSubjectTemplates()
if len(templates) != 1 {
t.Fatalf("expected 1 subject template, got %d", len(templates))
}

tpl := templates[0]
if tpl.Name != "aws-ec2-instance" {
t.Fatalf("unexpected template name: %s", tpl.Name)
}
if tpl.Type != proto.SubjectType_SUBJECT_TYPE_COMPONENT {
t.Fatalf("unexpected template type: %v", tpl.Type)
}

expectedIdentityKeys := []string{"provider", "region", "instance-id"}
if len(tpl.IdentityLabelKeys) != len(expectedIdentityKeys) {
t.Fatalf("unexpected identity key count: %d", len(tpl.IdentityLabelKeys))
}
for i, key := range expectedIdentityKeys {
if tpl.IdentityLabelKeys[i] != key {
t.Fatalf("unexpected identity key at %d: %s", i, tpl.IdentityLabelKeys[i])
}
}

if len(tpl.SelectorLabels) != 1 {
t.Fatalf("unexpected selector count: %d", len(tpl.SelectorLabels))
}
if tpl.SelectorLabels[0].Key != "type" || tpl.SelectorLabels[0].Value != "ec2" {
t.Fatalf("unexpected selector label: %s=%s", tpl.SelectorLabels[0].Key, tpl.SelectorLabels[0].Value)
}

expectedSchemaKeys := []string{"provider", "type", "instance-id", "_vpc-id", "_subnet-id", "region"}
if len(tpl.LabelSchema) != len(expectedSchemaKeys) {
t.Fatalf("unexpected label schema count: %d", len(tpl.LabelSchema))
}
for i, key := range expectedSchemaKeys {
if tpl.LabelSchema[i].Key != key {
t.Fatalf("unexpected label schema key at %d: %s", i, tpl.LabelSchema[i].Key)
Comment on lines +18 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify remaining direct proto field access in test file (should return no matches after fix)
rg -nP 'tpl\.(Name|Type|IdentityLabelKeys|SelectorLabels|LabelSchema)\b' subject_templates_test.go

Repository: compliance-framework/plugin-aws-ec2

Length of output: 1114


Fix protogetter lint failures by using protobuf getters in subject_templates_test.go
Direct proto field access (tpl.Name, tpl.Type, tpl.IdentityLabelKeys, tpl.SelectorLabels, tpl.LabelSchema, and their nested .Key/.Value) still exists in this test (lines 18-48) and will trip protogetter-style lint. Replace with GetX() accessors.

Proposed fix
-	if tpl.Name != "aws-ec2-instance" {
-		t.Fatalf("unexpected template name: %s", tpl.Name)
+	if tpl.GetName() != "aws-ec2-instance" {
+		t.Fatalf("unexpected template name: %s", tpl.GetName())
 	}
-	if tpl.Type != proto.SubjectType_SUBJECT_TYPE_COMPONENT {
-		t.Fatalf("unexpected template type: %v", tpl.Type)
+	if tpl.GetType() != proto.SubjectType_SUBJECT_TYPE_COMPONENT {
+		t.Fatalf("unexpected template type: %v", tpl.GetType())
 	}

 	expectedIdentityKeys := []string{"provider", "region", "instance-id"}
-	if len(tpl.IdentityLabelKeys) != len(expectedIdentityKeys) {
-		t.Fatalf("unexpected identity key count: %d", len(tpl.IdentityLabelKeys))
+	if len(tpl.GetIdentityLabelKeys()) != len(expectedIdentityKeys) {
+		t.Fatalf("unexpected identity key count: %d", len(tpl.GetIdentityLabelKeys()))
 	}
 	for i, key := range expectedIdentityKeys {
-		if tpl.IdentityLabelKeys[i] != key {
-			t.Fatalf("unexpected identity key at %d: %s", i, tpl.IdentityLabelKeys[i])
+		if tpl.GetIdentityLabelKeys()[i] != key {
+			t.Fatalf("unexpected identity key at %d: %s", i, tpl.GetIdentityLabelKeys()[i])
 		}
 	}

-	if len(tpl.SelectorLabels) != 1 {
-		t.Fatalf("unexpected selector count: %d", len(tpl.SelectorLabels))
+	if len(tpl.GetSelectorLabels()) != 1 {
+		t.Fatalf("unexpected selector count: %d", len(tpl.GetSelectorLabels()))
 	}
-	if tpl.SelectorLabels[0].Key != "type" || tpl.SelectorLabels[0].Value != "ec2" {
-		t.Fatalf("unexpected selector label: %s=%s", tpl.SelectorLabels[0].Key, tpl.SelectorLabels[0].Value)
+	if tpl.GetSelectorLabels()[0].GetKey() != "type" || tpl.GetSelectorLabels()[0].GetValue() != "ec2" {
+		t.Fatalf("unexpected selector label: %s=%s", tpl.GetSelectorLabels()[0].GetKey(), tpl.GetSelectorLabels()[0].GetValue())
 	}

 	expectedSchemaKeys := []string{"provider", "type", "instance-id", "_vpc-id", "_subnet-id", "region"}
-	if len(tpl.LabelSchema) != len(expectedSchemaKeys) {
-		t.Fatalf("unexpected label schema count: %d", len(tpl.LabelSchema))
+	if len(tpl.GetLabelSchema()) != len(expectedSchemaKeys) {
+		t.Fatalf("unexpected label schema count: %d", len(tpl.GetLabelSchema()))
 	}
 	for i, key := range expectedSchemaKeys {
-		if tpl.LabelSchema[i].Key != key {
-			t.Fatalf("unexpected label schema key at %d: %s", i, tpl.LabelSchema[i].Key)
+		if tpl.GetLabelSchema()[i].GetKey() != key {
+			t.Fatalf("unexpected label schema key at %d: %s", i, tpl.GetLabelSchema()[i].GetKey())
 		}
 	}
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 18-18: avoid direct access to proto field tpl.Name, use tpl.GetName() instead

(protogetter)


[error] 19-19: avoid direct access to proto field tpl.Name, use tpl.GetName() instead

(protogetter)


[error] 21-21: avoid direct access to proto field tpl.Type, use tpl.GetType() instead

(protogetter)


[error] 22-22: avoid direct access to proto field tpl.Type, use tpl.GetType() instead

(protogetter)


[error] 26-26: avoid direct access to proto field tpl.IdentityLabelKeys, use tpl.GetIdentityLabelKeys() instead

(protogetter)


[error] 27-27: avoid direct access to proto field tpl.IdentityLabelKeys, use tpl.GetIdentityLabelKeys() instead

(protogetter)


[error] 30-30: avoid direct access to proto field tpl.IdentityLabelKeys, use tpl.GetIdentityLabelKeys() instead

(protogetter)


[error] 35-35: avoid direct access to proto field tpl.SelectorLabels, use tpl.GetSelectorLabels() instead

(protogetter)


[error] 36-36: avoid direct access to proto field tpl.SelectorLabels, use tpl.GetSelectorLabels() instead

(protogetter)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subject_templates_test.go` around lines 18 - 48, Replace direct proto field
accesses on tpl in subject_templates_test.go with the protobuf getter methods:
use tpl.GetName() instead of tpl.Name, tpl.GetType() instead of tpl.Type,
tpl.GetIdentityLabelKeys() for IdentityLabelKeys, tpl.GetSelectorLabels() for
SelectorLabels, and tpl.GetLabelSchema() for LabelSchema; for nested label
entries use entry.GetKey() and entry.GetValue() instead of .Key/.Value. Update
the comparisons and loops to call these getters (e.g., iterate over
tpl.GetIdentityLabelKeys(), compare
tpl.GetSelectorLabels()[0].GetKey()/GetValue(), and
tpl.GetLabelSchema()[i].GetKey()) so the test uses protogetter accessors
throughout.

}
}
}

func TestBuildSubjectTemplatesRenderCurrentLabels(t *testing.T) {
tpl := buildSubjectTemplates()[0]
labels := map[string]string{
"provider": "aws",
"type": "ec2",
"region": "eu-west-2",
"instance-id": "i-0123456789abcdef0",
"_vpc-id": "vpc-0123456789abcdef0",
"_subnet-id": "subnet-0123456789abcdef0",
}

title := executeTemplate(t, tpl.TitleTemplate, labels)
if title != "AWS EC2 instance i-0123456789abcdef0 in eu-west-2" {
t.Fatalf("unexpected title: %s", title)
}

description := executeTemplate(t, tpl.DescriptionTemplate, labels)
expectedDescription := "Amazon EC2 instance i-0123456789abcdef0 in subnet subnet-0123456789abcdef0 and VPC vpc-0123456789abcdef0 within AWS region eu-west-2."
if description != expectedDescription {
t.Fatalf("unexpected description: %s", description)
}
}

func executeTemplate(t *testing.T, source string, data map[string]string) string {
t.Helper()

tpl, err := template.New("subject").Parse(source)
if err != nil {
t.Fatalf("parse template: %v", err)
}

var buf bytes.Buffer
if err := tpl.Execute(&buf, data); err != nil {
t.Fatalf("execute template: %v", err)
}

return buf.String()
}
Loading