Skip to content

Commit

Permalink
Merge pull request #1200 from guardian/nt/fsbp-oblig-2
Browse files Browse the repository at this point in the history
Create obligatron mode for AWS infrastructure vulnerabilities
  • Loading branch information
NovemberTang authored Jul 12, 2024
2 parents ba13195 + 069a66b commit eb35b3e
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 3 deletions.
39 changes: 39 additions & 0 deletions packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -20544,6 +20544,45 @@ spec:
},
"Type": "AWS::Lambda::Function",
},
"obligatronAWSVULNERABILITIES8E81FCF9": {
"Properties": {
"Description": "Daily execution of Obligatron lambda for 'AWS_VULNERABILITIES' obligation",
"ScheduleExpression": "cron(0 11 * * ? *)",
"State": "ENABLED",
"Targets": [
{
"Arn": {
"Fn::GetAtt": [
"obligatronA58CFCF1",
"Arn",
],
},
"Id": "Target0",
"Input": ""AWS_VULNERABILITIES"",
},
],
},
"Type": "AWS::Events::Rule",
},
"obligatronAWSVULNERABILITIESAllowEventRuleServiceCatalogueobligatron5BD5033E8F5FADB8": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"obligatronA58CFCF1",
"Arn",
],
},
"Principal": "events.amazonaws.com",
"SourceArn": {
"Fn::GetAtt": [
"obligatronAWSVULNERABILITIES8E81FCF9",
"Arn",
],
},
},
"Type": "AWS::Lambda::Permission",
},
"obligatronErrorPercentageAlarmForLambda8AFDF23F": {
"Properties": {
"ActionsEnabled": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export function daysLeftToFix(vuln: RepocopVulnerability): number | undefined {

export function toNonEmptyArray<T>(value: T[]): NonEmptyArray<T> {
if (value.length === 0) {
throw new Error(`Expected a non-empty array. Source table may be empty.`);
throw new Error(`Expected a non-empty array.`);
}
return value as NonEmptyArray<T>;
}
4 changes: 4 additions & 0 deletions packages/obligatron/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Obligations,
stringIsObligation,
} from './obligations';
import { evaluateFsbpVulnerabilities } from './obligations/aws-vulnerabilities';
import { evaluateDependencyVulnerabilityObligation } from './obligations/dependency-vulnerabilities';
import {
evaluateAmiTaggingCoverage,
Expand All @@ -31,6 +32,9 @@ async function getResults(
case 'PRODUCTION_DEPENDENCIES': {
return await evaluateDependencyVulnerabilityObligation(db);
}
case 'AWS_VULNERABILITIES': {
return await evaluateFsbpVulnerabilities(db);
}
}
}

Expand Down
81 changes: 81 additions & 0 deletions packages/obligatron/src/obligations/aws-vulnerabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { AwsContact } from '../obligations/index';
import {
fsbpFindingsToObligatronResults,
type SecurityHubFinding,
} from './aws-vulnerabilities';

describe('The dependency vulnerabilities obligation', () => {
const resource1 = {
Id: 'arn:service:1',
Tags: { Stack: 'myStack' },
Region: 'some-region',
Type: 'some-type',
};

const resource2 = {
...resource1,
Id: 'arn:service:2',
};

const oneResourceFinding: SecurityHubFinding = {
resources: [resource1],
severity: { Label: 'HIGH', Normalized: 75 },
aws_account_id: '0123456',
first_observed_at: new Date('2020-01-01'),
product_fields: { ControlId: 'S.1', StandardsArn: 'arn:1' },
};

const twoResourceFinding: SecurityHubFinding = {
...oneResourceFinding,
resources: [resource1, resource2],
};

it('should return a result in the expected format', () => {
const actual = fsbpFindingsToObligatronResults([oneResourceFinding]);

const expected = {
contacts: {
App: undefined,
Stack: 'myStack',
Stage: undefined,
aws_account_id: '0123456',
},
reason: 'The following AWS FSBP controls are failing: S.1',
resource: 'arn:service:1',
url: 'https://docs.aws.amazon.com/securityhub/latest/userguide/fsbp-standard.html',
};

expect(actual).toEqual([expected]);
});

it('should return multiple results if two resources are referenced in the same finding', () => {
const actual = fsbpFindingsToObligatronResults([twoResourceFinding]);
expect(actual.length).toEqual(2);
});

it('should list multiple control IDs in one finding if the same resource has failed two controls', () => {
const extraFinding = {
...oneResourceFinding,
product_fields: { ControlId: 'S.2', StandardsArn: 'arn:1' },
};

const actual = fsbpFindingsToObligatronResults([
oneResourceFinding,
extraFinding,
])[0]?.reason;

expect(actual).toContain('S.1');
expect(actual).toContain('S.2');
});
it('should handle a resource with no tags', () => {
const finding = {
...oneResourceFinding,
resources: [{ ...resource1, Tags: {} }],
};

const actual = fsbpFindingsToObligatronResults([finding]);
const contacts = actual[0]?.contacts as AwsContact;

expect(contacts.Stack).toBeUndefined();
});
});
111 changes: 111 additions & 0 deletions packages/obligatron/src/obligations/aws-vulnerabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { aws_securityhub_findings, PrismaClient } from '@prisma/client';
import { getFsbpFindings } from 'common/src/database-queries';
import { toNonEmptyArray } from 'common/src/functions';
import type { ObligationResult } from '.';

type Resource = {
Id: string;
Tags: Record<string, string>;
Region: string;
Type: string;
};

type ProductFields = {
ControlId: string;
StandardsArn: string;
};

export type SecurityHubFinding = Pick<
aws_securityhub_findings,
'first_observed_at' | 'aws_account_id'
> & {
severity: { Label: string; Normalized: number };
resources: Resource[];
product_fields: ProductFields;
};

type Failure = {
resource: string;
controlId: string;
accountId: string;
tags: Record<string, string> | null;
};

function findingToFailures(finding: SecurityHubFinding): Failure[] {
return finding.resources.map((resource) => ({
resource: resource.Id,
controlId: finding.product_fields.ControlId,
accountId: finding.aws_account_id,
tags: resource.Tags,
}));
}

function groupFailuresByResource(
failures: Failure[],
): Record<string, Failure[]> {
const grouped: Record<string, Failure[]> = {};

for (const failure of failures) {
if (!grouped[failure.resource]) {
grouped[failure.resource] = [];
}

grouped[failure.resource]?.push(failure);
}

return grouped;
}

function failuresToObligationResult(
arn: string,
failures: Failure[],
): ObligationResult {
const oneFailure = toNonEmptyArray(failures)[0];

const controlIds: string[] = failures.map((f) => f.controlId);
const accountId: string | undefined = oneFailure.accountId;
const tags = oneFailure.tags;
return {
resource: arn,
reason: `The following AWS FSBP controls are failing: ${controlIds.join(', ')}`,
url: 'https://docs.aws.amazon.com/securityhub/latest/userguide/fsbp-standard.html',
contacts: {
aws_account_id: accountId,
Stack: tags === null ? undefined : tags.Stack,
Stage: tags === null ? undefined : tags.Stage,
App: tags === null ? undefined : tags.App,
},
};
}

function failuresToObligationResults(
failuresByResource: Record<string, Failure[]>,
): ObligationResult[] {
return Object.entries(failuresByResource).map(([resource, failures]) =>
failuresToObligationResult(resource, failures),
);
}

//TODO filter out findings that are within the SLA window
export function fsbpFindingsToObligatronResults(
findings: SecurityHubFinding[],
): ObligationResult[] {
const allFailures = findings.flatMap(findingToFailures);
const failuresByResource = groupFailuresByResource(allFailures);
return failuresToObligationResults(failuresByResource);
}

export async function evaluateFsbpVulnerabilities(
client: PrismaClient,
): Promise<ObligationResult[]> {
const findings = (await getFsbpFindings(client, ['CRITICAL', 'HIGH'])).map(
(v) => v as unknown as SecurityHubFinding,
);

console.log(`Found ${findings.length} FSBP findings`);

const results = fsbpFindingsToObligatronResults(findings);
console.log(results.slice(0, 5));

return [];
}
8 changes: 6 additions & 2 deletions packages/obligatron/src/obligations/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
// Slightly hacky file to allow CDK project to import the list of obligations without having to compile the whole Obligatron project

export const Obligations = ['TAGGING', 'PRODUCTION_DEPENDENCIES'] as const;
export const Obligations = [
'TAGGING',
'PRODUCTION_DEPENDENCIES',
'AWS_VULNERABILITIES',
] as const;
export type Obligation = (typeof Obligations)[number];

export const stringIsObligation = (input: string): input is Obligation => {
return Obligations.filter((v) => v === input).length > 0;
};

type AwsContact = {
export type AwsContact = {
aws_account_id?: string;
Stack?: string;
Stage?: string;
Expand Down

0 comments on commit eb35b3e

Please sign in to comment.