diff --git a/help/cli-commands/iac-gen-driftignore.md b/help/cli-commands/iac-gen-driftignore.md index 1c978bc669..dfe3c6afae 100644 --- a/help/cli-commands/iac-gen-driftignore.md +++ b/help/cli-commands/iac-gen-driftignore.md @@ -28,14 +28,6 @@ Use the `-d` option to output the debug logs. ## Options -### `--input` - -Input where the JSON should be parsed from. Defaults to stdin. - -### `--output` - -Output file path to write the driftignore to. (default ".driftignore") - ### `--exclude-changed` Exclude resources that changed on cloud provider @@ -52,5 +44,5 @@ Exclude resources not managed by IaC ``` $ snyk iac scan --output=json://output.json -$ snyk iac gen-driftignore --input=output.json --output=/dev/stdout +$ cat output.json | snyk iac gen-driftignore ``` diff --git a/src/cli/commands/gen-driftignore.ts b/src/cli/commands/gen-driftignore.ts index 964444ed5d..85e354e00c 100644 --- a/src/cli/commands/gen-driftignore.ts +++ b/src/cli/commands/gen-driftignore.ts @@ -1,10 +1,15 @@ import { MethodArgs } from '../args'; import { processCommandArgs } from './process-command-args'; import * as legacyError from '../../lib/errors/legacy-errors'; -import { runDriftCTL } from '../../lib/iac/drift'; +import * as fs from 'fs'; +import * as snykPolicyLib from 'snyk-policy'; import { getIacOrgSettings } from './test/iac-local-execution/org-settings/get-iac-org-settings'; import { UnsupportedEntitlementCommandError } from './test/iac-local-execution/assert-iac-options-flag'; import config from '../../lib/config'; +import { + parseDriftAnalysisResults, + updateExcludeInPolicy, +} from '../../lib/iac/drift'; export default async (...args: MethodArgs): Promise => { const { options } = processCommandArgs(...args); @@ -24,11 +29,10 @@ export default async (...args: MethodArgs): Promise => { } try { - const ret = await runDriftCTL({ - options: { kind: 'gen-driftignore', ...options }, - stdio: 'inherit', - }); - process.exit(ret.code); + const analysis = parseDriftAnalysisResults(fs.readFileSync(0).toString()); + const policy = await snykPolicyLib.load(); + await updateExcludeInPolicy(policy, analysis, options); + await snykPolicyLib.save(policy); } catch (e) { const err = new Error('Error running `iac gen-driftignore` ' + e); return Promise.reject(err); diff --git a/src/lib/iac/drift.ts b/src/lib/iac/drift.ts index 8feee54e8d..d9c4eb1b6e 100644 --- a/src/lib/iac/drift.ts +++ b/src/lib/iac/drift.ts @@ -9,6 +9,7 @@ import { makeRequest } from '../request'; import config from '../../lib/config'; import * as path from 'path'; import * as crypto from 'crypto'; + import { createIgnorePattern, verifyServiceMappingExists, @@ -118,10 +119,6 @@ export const generateArgs = (options: DriftCTLOptions): string[] => { return generateScanFlags(options as DescribeOptions); } - if (options.kind === 'gen-driftignore') { - return generateGenDriftIgnoreFlags(options as GenDriftIgnoreOptions); - } - if (options.kind === 'fmt') { return generateFmtFlags(options as FmtOptions); } @@ -129,36 +126,6 @@ export const generateArgs = (options: DriftCTLOptions): string[] => { throw 'Unsupported command'; }; -export const generateGenDriftIgnoreFlags = ( - options: GenDriftIgnoreOptions, -): string[] => { - const args: string[] = ['gen-driftignore', ...driftctlDefaultOptions]; - - if (options.input) { - args.push('--input'); - args.push(options.input); - } - - if (options.output) { - args.push('--output'); - args.push(options.output); - } - - if (options['exclude-changed']) { - args.push('--exclude-changed'); - } - - if (options['exclude-missing']) { - args.push('--exclude-missing'); - } - - if (options['exclude-unmanaged']) { - args.push('--exclude-unmanaged'); - } - - return args; -}; - const generateFmtFlags = (options: FmtOptions): string[] => { const args: string[] = ['fmt', ...driftctlDefaultOptions]; @@ -539,3 +506,30 @@ export function driftignoreFromPolicy(policy: Policy | undefined): string[] { } return policy.exclude[excludeSection]; } + +export const updateExcludeInPolicy = ( + policy: Policy, + analysis: DriftAnalysis, + options: GenDriftIgnoreOptions, +): void => { + const excludedResources = driftignoreFromPolicy(policy); + const addResource = (res) => excludedResources.push(`${res.type}.${res.id}`); + + if (!options['exclude-changed'] && analysis.summary.total_changed > 0) { + analysis.differences?.forEach((change) => addResource(change.res)); + } + + if (!options['exclude-missing'] && analysis.summary.total_missing > 0) { + analysis.missing?.forEach((res) => addResource(res)); + } + + if (!options['exclude-unmanaged'] && analysis.summary.total_unmanaged > 0) { + analysis.unmanaged?.forEach((res) => addResource(res)); + } + + if (!policy.exclude) { + policy.exclude = {}; + } + + policy.exclude['iac-drift'] = excludedResources; +}; diff --git a/src/lib/iac/types.d.ts b/src/lib/iac/types.d.ts index a7fc91db47..9943d10f04 100644 --- a/src/lib/iac/types.d.ts +++ b/src/lib/iac/types.d.ts @@ -14,9 +14,7 @@ export interface FmtOptions extends DriftCTLOptions { 'html-file-output': string; } -export interface GenDriftIgnoreOptions extends DriftCTLOptions { - input?: string; - output?: string; +export interface GenDriftIgnoreOptions { 'exclude-changed'?: boolean; 'exclude-missing'?: boolean; 'exclude-unmanaged'?: boolean; diff --git a/test/fixtures/iac/drift/analysis.json b/test/fixtures/iac/drift/analysis.json new file mode 100644 index 0000000000..8e26ffcccb --- /dev/null +++ b/test/fixtures/iac/drift/analysis.json @@ -0,0 +1,75 @@ +{ + "options": { + "deep": true, + "only_managed": false, + "only_unmanaged": false + }, + "summary": { + "total_resources": 6, + "total_changed": 1, + "total_unmanaged": 2, + "total_missing": 2, + "total_managed": 2, + "total_iac_source_count": 3 + }, + "managed": [ + { + "id": "AKIA5QYBVVD25KFXJHYJ", + "type": "aws_iam_access_key" + }, + { + "id": "test-managed", + "type": "aws_iam_user" + } + ], + "unmanaged": [ + { + "id": "driftctl", + "type": "aws_s3_bucket_policy" + }, + { + "id": "driftctl", + "type": "aws_s3_bucket_notification" + } + ], + "missing": [ + { + "id": "test-driftctl2", + "type": "aws_iam_user" + }, + { + "id": "AKIA5QYBVVD2Y6PBAAPY", + "type": "aws_iam_access_key" + } + ], + "differences": [ + { + "res": { + "id": "AKIA5QYBVVD25KFXJHYJ", + "type": "aws_iam_access_key" + }, + "changelog": [ + { + "type": "update", + "path": [ + "status" + ], + "from": "Active", + "to": "Inactive", + "computed": false + } + ] + } + ], + "coverage": 33, + "alerts": { + "aws_iam_access_key": [ + { + "message": "This is an alert" + } + ] + }, + "scan_duration": 123, + "provider_name": "AWS", + "provider_version": "2.18.5" +} \ No newline at end of file diff --git a/test/jest/acceptance/iac/gen-driftignore.spec.ts b/test/jest/acceptance/iac/gen-driftignore.spec.ts index c9d1e42b8e..e40a694168 100644 --- a/test/jest/acceptance/iac/gen-driftignore.spec.ts +++ b/test/jest/acceptance/iac/gen-driftignore.spec.ts @@ -1,25 +1,20 @@ -import { startMockServer } from './helpers'; +import { run as Run, startMockServer } from './helpers'; import * as os from 'os'; -import * as path from 'path'; -import { getFixturePath } from '../../util/getFixturePath'; import * as fs from 'fs'; -import * as uuid from 'uuid'; import * as rimraf from 'rimraf'; +import * as path from 'path'; +import { findAndLoadPolicy } from '../../../../src/lib/policy'; jest.setTimeout(50000); describe('iac gen-driftignore', () => { - let run: ( - cmd: string, - env: Record, - ) => Promise<{ stdout: string; stderr: string; exitCode: number }>; + let run: typeof Run; let teardown: () => void; let tmpFolderPath: string; - let outputFile: string; beforeEach(() => { tmpFolderPath = fs.mkdtempSync(path.join(os.tmpdir(), 'dctl-')); - outputFile = path.join(tmpFolderPath, uuid.v4()); + fs.closeSync(fs.openSync(path.join(tmpFolderPath, '.snyk'), 'w')); }); afterEach(() => { rimraf.sync(tmpFolderPath); @@ -48,26 +43,35 @@ describe('iac gen-driftignore', () => { return; // skip following tests } - it('gen-driftignore successfully executed from SNYK_DRIFTCTL_PATH env var when org has the entitlement', async () => { - const { stderr, exitCode } = await run( - `snyk iac gen-driftignore --input=something.json --output=stdout --exclude-changed --exclude-missing --exclude-unmanaged`, - { - SNYK_FIXTURE_OUTPUT_PATH: outputFile, - SNYK_DRIFTCTL_PATH: path.join( - getFixturePath('iac'), - 'drift', - 'args-echo.sh', - ), - }, + it('gen-driftignore successfully executed when org has the entitlement', async () => { + const analysisPath = path.join( + __dirname, + '../../../fixtures/iac/drift/analysis.json', ); - const output = fs.readFileSync(outputFile).toString(); + const snykBinaryPath = path.join(__dirname, '../../../../bin/snyk'); - expect(output).toContain('DCTL_IS_SNYK=true'); - expect(output).toContain( - 'ARGS=gen-driftignore --no-version-check --input something.json --output stdout --exclude-changed --exclude-missing --exclude-unmanaged', + const { stderr, stdout, exitCode } = await run( + `cat ${analysisPath} | ${snykBinaryPath} iac gen-driftignore`, + {}, + tmpFolderPath, ); + + const policy = await findAndLoadPolicy(tmpFolderPath, 'iac', {}); + const expectedExcludes = { + 'iac-drift': [ + 'aws_iam_access_key.AKIA5QYBVVD25KFXJHYJ', + 'aws_iam_user.test-driftctl2', + 'aws_iam_access_key.AKIA5QYBVVD2Y6PBAAPY', + 'aws_s3_bucket_policy.driftctl', + 'aws_s3_bucket_notification.driftctl', + ], + }; + + expect(stdout).toBe(''); expect(stderr).toMatch(''); expect(exitCode).toBe(0); + expect(policy).toBeDefined(); + expect(policy?.exclude).toStrictEqual(expectedExcludes); }); }); diff --git a/test/jest/unit/lib/iac/drift.spec.ts b/test/jest/unit/lib/iac/drift.spec.ts index 0d4cca6855..cf8e28ae29 100644 --- a/test/jest/unit/lib/iac/drift.spec.ts +++ b/test/jest/unit/lib/iac/drift.spec.ts @@ -7,6 +7,7 @@ import { generateArgs, parseDriftAnalysisResults, translateExitCode, + updateExcludeInPolicy, validateArgs, } from '../../../../../src/lib/iac/drift'; import envPaths from 'env-paths'; @@ -48,11 +49,6 @@ describe('driftctl integration', () => { ]); }); - it('gen-driftignore: default arguments are correct', () => { - const args = generateArgs({ kind: 'gen-driftignore' }); - expect(args).toEqual(['gen-driftignore', '--no-version-check']); - }); - it('describe: passing options generate correct arguments', () => { const args = generateArgs({ kind: 'describe', @@ -149,29 +145,6 @@ describe('driftctl integration', () => { }).toThrow(new DescribeExclusiveArgumentError()); }); - it('gen-driftignore: passing options generate correct arguments', () => { - const args = generateArgs({ - kind: 'gen-driftignore', - 'exclude-changed': true, - 'exclude-missing': true, - 'exclude-unmanaged': true, - input: 'analysis.json', - output: '/dev/stdout', - org: 'testing-org', // Ensure that this should not be translated to args - } as GenDriftIgnoreOptions); - expect(args).toEqual([ - 'gen-driftignore', - '--no-version-check', - '--input', - 'analysis.json', - '--output', - '/dev/stdout', - '--exclude-changed', - '--exclude-missing', - '--exclude-unmanaged', - ]); - }); - it('run driftctl: exit code is translated', () => { expect(translateExitCode(DCTL_EXIT_CODES.EXIT_IN_SYNC)).toEqual(0); expect(translateExitCode(DCTL_EXIT_CODES.EXIT_NOT_IN_SYNC)).toEqual( @@ -317,37 +290,164 @@ describe('drift analytics', () => { expect(addAnalyticsSpy).toHaveBeenCalledWith('iac-drift-scan-scope', 'all'); }); }); -describe('driftignoreFromPolicy', () => { - const loadPolicy = async (name: string): Promise => { - const policyPath = path.join(__dirname, 'fixtures', name); - const policyText = fs.readFileSync(policyPath, 'utf-8'); - return await snykPolicy.loadFromText(policyText); - }; +const loadPolicyFixture = async (name: string): Promise => { + const policyPath = path.join(__dirname, 'fixtures', name); + const policyText = fs.readFileSync(policyPath, 'utf-8'); + return await snykPolicy.loadFromText(policyText); +}; + +describe('driftignoreFromPolicy', () => { it.each([ ['policy undefined', undefined, []], - ['policy with no excludes', loadPolicy('policy-no-excludes.yml'), []], + [ + 'policy with no excludes', + loadPolicyFixture('policy-no-excludes.yml'), + [], + ], [ 'policy with irrelevant excludes', - loadPolicy('policy-irrelevant-excludes.yml'), + loadPolicyFixture('policy-irrelevant-excludes.yml'), [], ], [ 'policy with empty drift excludes', - loadPolicy('policy-empty-drift-excludes.yml'), + loadPolicyFixture('policy-empty-drift-excludes.yml'), [], ], [ 'policy with one drift exclude', - loadPolicy('policy-one-drift-exclude.yml'), + loadPolicyFixture('policy-one-drift-exclude.yml'), ['foo'], ], [ 'policy with several drift excludes', - loadPolicy('policy-several-drift-excludes.yml'), + loadPolicyFixture('policy-several-drift-excludes.yml'), ['*', '!aws_iam_*', 'aws_s3_*', 'aws_s3_bucket.*', 'aws_s3_bucket.name*'], ], ])('%s', async (_, policy, expected) => { expect(driftignoreFromPolicy(await policy)).toEqual(expected); }); }); + +describe('updateExcludeInPolicy', () => { + const analysis = parseDriftAnalysisResults( + fs.readFileSync( + path.join(__dirname, 'fixtures', 'driftctl-analysis.json'), + 'utf-8', + ), + ); + it.each([ + [ + 'policy with no excludes', + 'policy-no-excludes.yml', + {}, + { + 'iac-drift': [ + 'aws_iam_access_key.AKIA5QYBVVD25KFXJHYJ', + 'aws_iam_user.test-driftctl2', + 'aws_iam_access_key.AKIA5QYBVVD2Y6PBAAPY', + 'aws_s3_bucket_policy.driftctl', + 'aws_s3_bucket_notification.driftctl', + ], + }, + ], + [ + 'policy with irrelevant excludes', + 'policy-irrelevant-excludes.yml', + {}, + { + foo: ['bar'], + 'iac-drift': [ + 'aws_iam_access_key.AKIA5QYBVVD25KFXJHYJ', + 'aws_iam_user.test-driftctl2', + 'aws_iam_access_key.AKIA5QYBVVD2Y6PBAAPY', + 'aws_s3_bucket_policy.driftctl', + 'aws_s3_bucket_notification.driftctl', + ], + }, + ], + [ + 'policy with empty drift excludes', + 'policy-empty-drift-excludes.yml', + {}, + { + 'iac-drift': [ + 'aws_iam_access_key.AKIA5QYBVVD25KFXJHYJ', + 'aws_iam_user.test-driftctl2', + 'aws_iam_access_key.AKIA5QYBVVD2Y6PBAAPY', + 'aws_s3_bucket_policy.driftctl', + 'aws_s3_bucket_notification.driftctl', + ], + }, + ], + [ + 'policy with several drift excludes', + 'policy-several-drift-excludes.yml', + {}, + { + 'iac-drift': [ + // Those are existing ones + '*', + '!aws_iam_*', + 'aws_s3_*', + 'aws_s3_bucket.*', + 'aws_s3_bucket.name*', + // Following exclude are the new ones + 'aws_iam_access_key.AKIA5QYBVVD25KFXJHYJ', + 'aws_iam_user.test-driftctl2', + 'aws_iam_access_key.AKIA5QYBVVD2Y6PBAAPY', + 'aws_s3_bucket_policy.driftctl', + 'aws_s3_bucket_notification.driftctl', + ], + }, + ], + [ + 'with exclude changed option', + 'policy-no-excludes.yml', + { + 'exclude-changed': true, + }, + { + 'iac-drift': [ + 'aws_iam_user.test-driftctl2', + 'aws_iam_access_key.AKIA5QYBVVD2Y6PBAAPY', + 'aws_s3_bucket_policy.driftctl', + 'aws_s3_bucket_notification.driftctl', + ], + }, + ], + [ + 'with exclude changed option', + 'policy-no-excludes.yml', + { + 'exclude-missing': true, + }, + { + 'iac-drift': [ + 'aws_iam_access_key.AKIA5QYBVVD25KFXJHYJ', + 'aws_s3_bucket_policy.driftctl', + 'aws_s3_bucket_notification.driftctl', + ], + }, + ], + [ + 'with exclude changed option', + 'policy-no-excludes.yml', + { + 'exclude-unmanaged': true, + }, + { + 'iac-drift': [ + 'aws_iam_access_key.AKIA5QYBVVD25KFXJHYJ', + 'aws_iam_user.test-driftctl2', + 'aws_iam_access_key.AKIA5QYBVVD2Y6PBAAPY', + ], + }, + ], + ])('%s', async (_, policyPath, options: GenDriftIgnoreOptions, expected) => { + const policy = await loadPolicyFixture(policyPath); + updateExcludeInPolicy(policy, analysis, options); + expect(policy.exclude).toEqual(expected); + }); +});