package termfence import ( "context" "errors" "testing" "time" "github.com/aws/aws-sdk-go-v2/aws" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/aws/smithy-go" smithymiddleware "github.com/aws/smithy-go/middleware" "github.com/carlosframework/platform/internal/boxid" ) var ( box = Box{Ref: "annie-welding", Enrollment: "enr-annie-1", Cloud: boxid.Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}} worker = Box{Ref: "bert-sawing", Enrollment: "enr-bert-1", Cloud: boxid.Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0bbb"}} now = time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC) ) func describe(owner, instance string, state types.InstanceStateName) *ec2.DescribeInstancesOutput { return &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{ OwnerId: aws.String(owner), Instances: []types.Instance{{InstanceId: aws.String(instance), State: &types.InstanceState{Name: state}}}, }}} } func withRequestID(out *ec2.DescribeInstancesOutput, id string) *ec2.DescribeInstancesOutput { var md smithymiddleware.Metadata awsmiddleware.SetRequestIDMetadata(&md, id) out.ResultMetadata = md return out } func TestATerminatedInstanceOfThisBoxIsAFence(t *testing.T) { out := withRequestID(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "req-1") rec, err := Verify(box, worker, FromDescribe(out, "i-0abc", "eu-west-1"), now) if err != nil { t.Fatal(err) } want := Record{V: 1, Box: "annie-welding", Enrollment: "enr-annie-1", Cloud: box.Cloud, State: "terminated", ObservedAt: "2026-10-06T12:00:00Z", Via: "describe", RequestID: "req-1", WrittenBy: WrittenBy{Box: "bert-sawing", Enrollment: "enr-bert-1"}} if rec != want { t.Fatalf("record = %+v\nwant %+v", rec, want) } } // Every lookalike is refused. func TestLookalikesAreNotEvidence(t *testing.T) { cases := map[string]Observation{ "stopped": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameStopped), "i-0abc", "eu-west-1"), "stopping": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameStopping), "i-0abc", "eu-west-1"), "shutting-down": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameShuttingDown), "i-0abc", "eu-west-1"), "running": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameRunning), "i-0abc", "eu-west-1"), "another account": FromDescribe(describe("999999999999", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"), "another region": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "us-east-1"), "another instance": FromDescribe(describe("111122223333", "i-0zzz", types.InstanceStateNameTerminated), "i-0zzz", "eu-west-1"), "not in the reply": FromDescribe(describe("111122223333", "i-0zzz", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"), "empty reply": FromDescribe(&ec2.DescribeInstancesOutput{}, "i-0abc", "eu-west-1"), "nil reply": FromDescribe(nil, "i-0abc", "eu-west-1"), "no state": FromDescribe(&ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{OwnerId: aws.String("111122223333"), Instances: []types.Instance{{InstanceId: aws.String("i-0abc")}}}}}, "i-0abc", "eu-west-1"), "unknown source": {Via: "cloudtrail", Found: true, InstanceID: "i-0abc", Account: "111122223333", Region: "eu-west-1", State: "terminated"}, "zero observation": {}, } for name, o := range cases { t.Run(name, func(t *testing.T) { if rec, err := Verify(box, worker, o, now); !errors.Is(err, ErrNotEvidence) { t.Fatalf("record = %+v, err = %v; want ErrNotEvidence", rec, err) } }) } } // Review Focus 5: the fence is a regional worker's own read in the box's // own region. A worker enrolled elsewhere is refused even when what it // read is a perfect terminated observation. func TestAWorkerInAnotherRegionNeverFences(t *testing.T) { far := worker far.Cloud.Region = "us-east-1" o := FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1") if _, err := Verify(box, far, o, now); !errors.Is(err, ErrNotEvidence) { t.Fatalf("err = %v, want ErrNotEvidence", err) } } func TestAnIncompleteRosterEntryIsNeverFenced(t *testing.T) { // A found, terminated observation whose every coordinate is empty, so // it MATCHES an empty cloud field for field. Only the guard on the // entry itself can refuse it. o := Observation{Via: ViaDescribe, Found: true, State: "terminated"} for name, b := range map[string]Box{ "empty cloud": {Ref: "annie-welding", Enrollment: "enr-annie-1"}, "no enrollment": {Ref: "annie-welding", Cloud: box.Cloud}, "no reference": {Enrollment: "enr-annie-1", Cloud: box.Cloud}, "not aws": {Ref: "annie-welding", Enrollment: "enr-annie-1", Cloud: boxid.Cloud{Provider: "gcp", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}}, } { t.Run(name, func(t *testing.T) { full := Observation{Via: ViaDescribe, Found: true, InstanceID: b.Cloud.InstanceID, Account: b.Cloud.Account, Region: b.Cloud.Region, State: "terminated"} for _, obs := range []Observation{o, full} { if _, err := Verify(b, worker, obs, now); !errors.Is(err, ErrNotEvidence) { t.Fatalf("err = %v, want ErrNotEvidence", err) } } }) } if _, err := Verify(box, Box{Ref: "bert-sawing"}, FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"), now); !errors.Is(err, ErrNotEvidence) { t.Fatalf("a worker with no proven identity: err = %v", err) } } const terminatedEvent = `{"version":"0","id":"ev-123","detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","time":"2026-10-06T11:59:00Z","region":"eu-west-1","resources":["arn:aws:ec2:eu-west-1:111122223333:instance/i-0abc"],"detail":{"instance-id":"i-0abc","state":"terminated"}}` func TestATerminatedEventIsAFence(t *testing.T) { o, err := FromEvent([]byte(terminatedEvent)) if err != nil { t.Fatal(err) } rec, err := Verify(box, worker, o, now) if err != nil { t.Fatal(err) } if rec.Via != "event" || rec.EventID != "ev-123" || rec.State != "terminated" || rec.RequestID != "" { t.Fatalf("record = %+v", rec) } } func TestEventsThatAreNotEC2StateChangesAreRefused(t *testing.T) { for name, body := range map[string]string{ "not JSON": `terminated`, "another source": `{"id":"e","detail-type":"EC2 Instance State-change Notification","source":"my.app","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`, "another detail-type": `{"id":"e","detail-type":"EC2 Spot Instance Interruption Warning","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`, "no event id": `{"detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`, } { t.Run(name, func(t *testing.T) { if _, err := FromEvent([]byte(body)); err == nil { t.Fatal("accepted") } }) } } func TestAShuttingDownEventIsNotEvidence(t *testing.T) { body := `{"id":"e","detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"shutting-down"}}` o, err := FromEvent([]byte(body)) if err != nil { t.Fatal(err) } if _, err := Verify(box, worker, o, now); !errors.Is(err, ErrNotEvidence) { t.Fatalf("err = %v, want ErrNotEvidence", err) } } // fakeEC2 answers DescribeInstances and records the region each call was // forced into. type fakeEC2 struct { out *ec2.DescribeInstancesOutput err error regions []string ids [][]string } func (f *fakeEC2) DescribeInstances(_ context.Context, in *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) { o := ec2.Options{Region: "client-default"} for _, fn := range optFns { fn(&o) } f.regions = append(f.regions, o.Region) f.ids = append(f.ids, in.InstanceIds) return f.out, f.err } func TestDescribeInstanceAsksInTheBoxsOwnRegion(t *testing.T) { f := &fakeEC2{out: describe("111122223333", "i-0abc", types.InstanceStateNameTerminated)} out, err := DescribeInstance(context.Background(), f, box.Cloud) if err != nil { t.Fatal(err) } if len(f.regions) != 1 || f.regions[0] != "eu-west-1" || len(f.ids[0]) != 1 || f.ids[0][0] != "i-0abc" { t.Fatalf("called in %v for %v", f.regions, f.ids) } if _, err := Verify(box, worker, FromDescribe(out, "i-0abc", "eu-west-1"), now); err != nil { t.Fatal(err) } } func TestDescribeInstanceNotFoundIsAbsenceAndOtherErrorsAreErrors(t *testing.T) { notFound := &fakeEC2{err: &smithy.GenericAPIError{Code: "InvalidInstanceID.NotFound", Message: "gone"}} out, err := DescribeInstance(context.Background(), notFound, box.Cloud) if err != nil { t.Fatalf("NotFound: err = %v", err) } if _, err := Verify(box, worker, FromDescribe(out, "i-0abc", "eu-west-1"), now); !errors.Is(err, ErrNotEvidence) { t.Fatalf("NotFound fenced the box: err = %v", err) } denied := &fakeEC2{err: &smithy.GenericAPIError{Code: "UnauthorizedOperation", Message: "no"}} if _, err := DescribeInstance(context.Background(), denied, box.Cloud); err == nil { t.Fatal("an access error read as an answer") } if _, err := DescribeInstance(context.Background(), &fakeEC2{}, boxid.Cloud{InstanceID: "i-0abc"}); err == nil { t.Fatal("described an incomplete cloud") } }