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
1 change: 1 addition & 0 deletions exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ module.exports = {
'publicAmi' : require(__dirname + '/plugins/aws/ec2/publicAmi.js'),
'encryptedAmi' : require(__dirname + '/plugins/aws/ec2/encryptedAmi.js'),
'amiHasTags' : require(__dirname + '/plugins/aws/ec2/amiHasTags.js'),
'amiNamingConvention' : require(__dirname + '/plugins/aws/ec2/amiNamingConvention.js'),
'oldAmi' : require(__dirname + '/plugins/aws/ec2/oldAmi.js'),
'instanceIamRole' : require(__dirname + '/plugins/aws/ec2/instanceIamRole.js'),
'ebsBackupEnabled' : require(__dirname + '/plugins/aws/ec2/ebsBackupEnabled.js'),
Expand Down
84 changes: 84 additions & 0 deletions plugins/aws/ec2/amiNamingConvention.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
var async = require('async');
var helpers = require('../../../helpers/aws');

module.exports = {
title: 'AMI Naming Conventions',
category: 'EC2',
domain: 'Compute',
severity: 'Low',
description: 'Ensure that Amazon Machine Images (AMIs) follow organizational naming conventions for tagging',
more_info: 'AMIs should follow a consistent naming convention using the Name tag to identify their purpose, environment, and region. This helps prevent accidental use of incorrect images, reduces operational errors, and improves resource management. Without proper naming conventions, teams may deploy instances with outdated or inappropriate AMIs, leading to security vulnerabilities or configuration issues.',
link: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html',
recommended_action: 'Update AMI Name tags to follow organizational naming conventions.',
apis: ['EC2:describeImages'],
settings: {
ami_naming_pattern: {
name: 'AMI Naming Pattern',
description: 'A regex pattern to validate AMI Name tag values. Default: ^ami-(ue1|uw1|uw2|ew1|ec1|an1|an2|as1|as2|se1)-(d|t|s|p)-([a-z0-9\\-]+)$',
regex: '^.*$',
default: '^ami-(ue1|uw1|uw2|ew1|ec1|an1|an2|as1|as2|se1)-(d|t|s|p)-([a-z0-9\\-]+)$'
}
},
realtime_triggers: ['ec2:CreateImage', 'ec2:CreateTags', 'ec2:DeleteTags', 'ec2:DeregisterImage'],

run: function(cache, settings, callback) {
var results = [];
var source = {};
var regions = helpers.regions(settings);
var awsOrGov = helpers.defaultPartition(settings);

var config = {
ami_naming_pattern: settings.ami_naming_pattern || this.settings.ami_naming_pattern.default
};

var namingPattern = new RegExp(config.ami_naming_pattern);

async.each(regions.ec2, function(region, rcb){
var describeImages = helpers.addSource(cache, source,
['ec2', 'describeImages', region]);

if (!describeImages) return rcb();

if (describeImages.err || !describeImages.data) {
helpers.addResult(results, 3,
'Unable to query for AMIs: ' + helpers.addError(describeImages), region);
return rcb();
}

if (!describeImages.data.length) {
helpers.addResult(results, 0, 'No AMIs found', region);
return rcb();
}

for (var ami of describeImages.data) {
if (!ami.ImageId) continue;

const arn = 'arn:' + awsOrGov + ':ec2:' + region + '::image/' + ami.ImageId;

if (!ami.Tags || !ami.Tags.length) {
helpers.addResult(results, 2,
'AMI does not have a name tag', region, arn);
continue;
}

var nameTag = ami.Tags.find(tag => tag.Key === 'Name');

if (!nameTag || !nameTag.Value) {
helpers.addResult(results, 2,
'AMI does not have a name tag', region, arn);
} else if (!namingPattern.test(nameTag.Value)) {
helpers.addResult(results, 2,
`AMI Name tag "${nameTag.Value}" does not follow organizational naming convention`, region, arn);
} else {
helpers.addResult(results, 0,
`AMI Name tag "${nameTag.Value}" follows organizational naming convention`, region, arn);
}
}

rcb();
}, function(){
callback(null, results, source);
});
}
};

132 changes: 132 additions & 0 deletions plugins/aws/ec2/amiNamingConvention.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
var expect = require('chai').expect;
const amiNamingConvention = require('./amiNamingConvention');

const describeImages = [
{
ImageId: 'ami-046b09f5340dfd8gb',
Tags: [
{ Key: 'Name', Value: 'ami-ue1-p-nodejs' }
]
},
{
ImageId: 'ami-046b09f5340dfd8gc',
Tags: [
{ Key: 'Name', Value: 'ami-uw2-d-apache-spark' }
]
},
{
ImageId: 'ami-046b09f5340dfd8gd',
Tags: [
{ Key: 'Name', Value: 'MyCustomAMI' }
]
},
{
ImageId: 'ami-046b09f5340dfd8ge',
Tags: [
{ Key: 'Environment', Value: 'Production' }
]
},
{
ImageId: 'ami-046b09f5340dfd8gf',
Tags: []
}
];

const createCache = (instances) => {
return {
ec2: {
describeImages: {
'us-east-1': {
data: instances
},
},
},
};
};

const createErrorCache = () => {
return {
ec2: {
describeImages: {
'us-east-1': {
err: {
message: 'error describing AMIs'
}
},
},
},
};
};


describe('amiNamingConvention', function () {
describe('run', function () {

it('should return UNKNOWN if unable to query for AMIs', function (done) {
const cache = createErrorCache();
amiNamingConvention.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(3);
expect(results[0].region).to.equal('us-east-1');
expect(results[0].message).to.include('Unable to query for AMIs');
done();
});
});

it('should return PASS if no AMIs found', function (done) {
const cache = createCache([]);
amiNamingConvention.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
expect(results[0].region).to.equal('us-east-1');
expect(results[0].message).to.include('No AMIs found');
done();
});
});

it('should return PASS if AMI Name tag follows naming convention', function (done) {
const cache = createCache([describeImages[0]]);
amiNamingConvention.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
expect(results[0].region).to.equal('us-east-1');
expect(results[0].message).to.include('follows organizational naming convention');
done();
});
});

it('should return FAIL if AMI Name tag does not follow naming convention', function (done) {
const cache = createCache([describeImages[2]]);
amiNamingConvention.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
expect(results[0].region).to.equal('us-east-1');
expect(results[0].message).to.include('does not follow organizational naming convention');
done();
});
});

it('should return FAIL if AMI does not have a name tag', function (done) {
const cache = createCache([describeImages[3]]);
amiNamingConvention.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
expect(results[0].region).to.equal('us-east-1');
expect(results[0].message).to.include('AMI does not have a name tag');
done();
});
});

it('should return FAIL if AMI has empty tags array', function (done) {
const cache = createCache([describeImages[4]]);
amiNamingConvention.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
expect(results[0].region).to.equal('us-east-1');
expect(results[0].message).to.include('AMI does not have a name tag');
done();
});
});
});
});