Skip to content

Commit

Permalink
Merge pull request snyk#2629 from snyk/feat/add-path-arg-for-ignore
Browse files Browse the repository at this point in the history
Feat: Add path argument for ignore [CFG-1489]
  • Loading branch information
ofekatr authored Feb 2, 2022
2 parents b9f311b + cc7803c commit ff894fe
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 16 deletions.
22 changes: 18 additions & 4 deletions help/cli-commands/ignore.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Usage

`snyk ignore --id=<ISSUE_ID> [--expiry=<EXPIRY>] [--reason=<REASON>] [<OPTIONS>]`
`snyk ignore --id=<ISSUE_ID> [--expiry=<EXPIRY>] [--path=<PATH_TO_RESOURCE>] [--reason=<REASON>] [<OPTIONS>]`

## Description

Expand All @@ -11,7 +11,7 @@ The `snyk ignore` command modifies the `.snyk` policy file to ignore a certain i
```yaml
ignore:
'<ISSUE_ID>':
- '*':
- '<PATH>':
reason: <REASON>
expires: <EXPIRY>
```
Expand All @@ -30,10 +30,24 @@ Snyk ID for the issue to ignore. Required.

Expiry date in a format `YYYY-MM-DD` ([ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) and [RFC 2822](https://tools.ietf.org/html/rfc2822) are supported).

### `--path=<PATH_TO_RESOURCE>`

Path to resource for which to ignore the issue. Use to narrow the scope of the ignore rule. When no resource path is specified, all resources are ignored.

### `--reason=<REASON>`

Human-readable `<REASON>` to ignore this issue.

## Example: ignore a specific vulnerability
## Examples

### Ignore a specific vulnerability:

`$ snyk ignore --id='npm:qs:20170213' --expiry='2021-01-10' --reason='Module not affected by this vulnerability'`
```
$ snyk ignore --id='npm:qs:20170213' --expiry='2021-01-10' --reason='Module not affected by this vulnerability'
```
### Ignore a specific vulnerability with a specified resource path:
```
$ snyk ignore --id='SNYK-JS-PATHPARSE-1077067' --expiry='2022-01-31' --path='[email protected] > [email protected] > [email protected]' --reason='Not currently exploitable.'
```
52 changes: 41 additions & 11 deletions src/cli/commands/ignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as authorization from '../../lib/authorization';
import * as auth from './auth/is-authed';
import { apiTokenExists } from '../../lib/api-token';
import { isCI } from '../../lib/is-ci';
import { MethodResult } from './types';
import { IgnoreRules, MethodResult } from './types';

import * as Debug from 'debug';
const debug = Debug('snyk');
Expand Down Expand Up @@ -46,9 +46,15 @@ export default function ignore(options): Promise<MethodResult> {
options.reason = 'None Given';
}

const isPathProvided = !!options.path;
if (!isPathProvided) {
options.path = '*';
}

debug(
'changing policy: ignore "%s", for all paths, reason: "%s", until: %o',
`changing policy: ignore "%s", for %s, reason: "%s", until: %o`,
options.id,
isPathProvided ? 'all paths' : `path: '${options.path}'`,
options.reason,
options.expiry,
);
Expand All @@ -62,15 +68,39 @@ export default function ignore(options): Promise<MethodResult> {
throw Error('policyFile');
})
.then(async function ignoreIssue(pol) {
pol.ignore[options.id] = [
{
'*': {
reason: options.reason,
expires: options.expiry,
created: new Date(),
},
},
];
let ignoreRulePathDataIdx = -1;
const ignoreParams = {
reason: options.reason,
expires: options.expiry,
created: new Date(),
};

const ignoreRules: IgnoreRules = pol.ignore;

const issueIgnorePaths = ignoreRules[options.id] ?? [];

// Checking if the an ignore rule for this issue exists for the provided path.
ignoreRulePathDataIdx = issueIgnorePaths.findIndex(
(ignoreMetadata) => !!ignoreMetadata[options.path],
);

// If an ignore rule for this path doesn't exist, create one.
if (ignoreRulePathDataIdx === -1) {
issueIgnorePaths.push({
[options.path]: ignoreParams,
});
}
// Otherwise, update the existing rule's metadata.
else {
issueIgnorePaths[ignoreRulePathDataIdx][
options.path
] = ignoreParams;
}

ignoreRules[options.id] = issueIgnorePaths;

pol.ignore = ignoreRules;

return await policy.save(pol, options['policy-path']);
});
});
Expand Down
14 changes: 14 additions & 0 deletions src/cli/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,17 @@ class JsonTestCommandResult extends TestCommandResult {
return this.sarifResult;
}
}

export interface IgnoreMetadata {
reason: string;
expires: Date;
created: Date;
}

export interface IgnoreRulePathData {
[path: string]: IgnoreMetadata;
}

export interface IgnoreRules {
[issueId: string]: IgnoreRulePathData[];
}
243 changes: 243 additions & 0 deletions test/jest/acceptance/snyk-ignore/snyk-ignore.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { load as loadPolicy } from 'snyk-policy';
import { fakeServer } from '../../../acceptance/fake-server';
import { createProjectFromWorkspace } from '../../util/createProject';
import { requireSnykToken } from '../../util/requireSnykToken';
import { runSnykCLI, runSnykCLIWithArray } from '../../util/runSnykCLI';

jest.setTimeout(1000 * 60);

describe('snyk ignore', () => {
let server: ReturnType<typeof fakeServer>;
let env: Record<string, string>;

beforeAll((done) => {
const apiPath = '/api/v1';
const apiPort = process.env.PORT || process.env.SNYK_PORT || '12345';
env = {
...process.env,
SNYK_API: 'http://localhost:' + apiPort + apiPath,
SNYK_HOST: 'http://localhost:' + apiPort,
SNYK_TOKEN: requireSnykToken(),
SNYK_DISABLE_ANALYTICS: '1',
};

server = fakeServer(apiPath, env.SNYK_TOKEN);
server.listen(apiPort, () => done());
});

afterEach(() => {
server.restore();
});

afterAll((done) => {
server.close(() => done());
});

it('creates a policy file using minimal options', async () => {
const project = await createProjectFromWorkspace('empty');
const { code } = await runSnykCLI(`ignore --id=ID`, {
cwd: project.path(),
env: env,
});

expect(code).toEqual(0);

const policy = await loadPolicy(project.path());
expect(policy).toMatchObject({
ignore: {
ID: [
{
'*': {
reason: 'None Given',
expires: expect.any(Date),
created: expect.any(Date),
},
},
],
},
});
});

it('creates a policy file using provided options', async () => {
const project = await createProjectFromWorkspace('empty');
const { code } = await runSnykCLI(
`ignore --id=ID --reason=REASON --expiry=2017-10-07 --policy-path=${project.path()}`,
{
cwd: project.path(),
env: env,
},
);

expect(code).toEqual(0);
const policy = await loadPolicy(project.path());
expect(policy).toMatchObject({
ignore: {
ID: [
{
'*': {
reason: 'REASON',
expires: new Date('2017-10-07'),
created: expect.any(Date),
},
},
],
},
});
});

it('correctly creates ignore rules for multiple paths', async () => {
// Arrange
const issueId = 'ID';

const paths = ['a > b > c[key] > d', 'x > y[key] > z'];

const dateStr = '2017-10-07';

const ignoreArgs = {
reason: 'REASON',
expires: new Date(dateStr),
};

const project = await createProjectFromWorkspace('empty');

// Act
await runSnykCLIWithArray(
[
'ignore',
`--id=${issueId}`,
`--path=${paths[0]}`,
`--expiry=${dateStr}`,
`--reason=${ignoreArgs.reason}`,
],
{
cwd: project.path(),
env: env,
},
);

await runSnykCLIWithArray(
[
'ignore',
`--id=${issueId}`,
`--path=${paths[1]}`,
`--expiry=${dateStr}`,
`--reason=${ignoreArgs.reason}`,
],
{
cwd: project.path(),
env: env,
},
);

// Assert
const policy = await loadPolicy(project.path());
expect(policy).toMatchObject({
ignore: {
[issueId]: [
{
[paths[0]]: {
...ignoreArgs,
created: expect.any(Date),
},
},
{
[paths[1]]: {
...ignoreArgs,
created: expect.any(Date),
},
},
],
},
});
});

it('correctly updates an ignore rule when given an existing resource paths', async () => {
// Arrange
const issueId = 'ID';

const path = 'a > b > c';

const dateStrs = ['2017-10-07', '2019-10-07'];

const ignoreArgs1 = {
reason: 'REASON1',
expires: new Date(dateStrs[0]),
};

const ignoreArgs2 = {
reason: 'REASON2',
expires: new Date(dateStrs[1]),
};

const project = await createProjectFromWorkspace('empty');

// Act
await runSnykCLIWithArray(
[
'ignore',
`--id=${issueId}`,
`--path=${path}`,
`--expiry=${dateStrs[0]}`,
`--reason=${ignoreArgs1.reason}`,
],
{
cwd: project.path(),
env: env,
},
);

await runSnykCLIWithArray(
[
'ignore',
`--id=${issueId}`,
`--path=${path}`,
`--expiry=${dateStrs[1]}`,
`--reason=${ignoreArgs2.reason}`,
],
{
cwd: project.path(),
env: env,
},
);

// Assert
const policy = await loadPolicy(project.path());

expect(policy).toMatchObject({
ignore: {
[issueId]: [
{
[path]: {
...ignoreArgs2,
created: expect.any(Date),
},
},
],
},
});
});

it('fails on missing ID', async () => {
const project = await createProjectFromWorkspace('empty');
const { code, stdout } = await runSnykCLI(`ignore --reason=REASON`, {
cwd: project.path(),
env: env,
});

expect(code).toEqual(2);
expect(stdout).toMatch('id is a required field');
});

it('errors when user is not authorized to ignore', async () => {
const project = await createProjectFromWorkspace('empty');
server.unauthorizeAction('cliIgnore', 'not allowed');

const { code, stdout } = await runSnykCLI(`ignore --id=ID`, {
cwd: project.path(),
env,
});

expect(code).toEqual(0);
expect(stdout).toMatch('not allowed');
});
});
Loading

0 comments on commit ff894fe

Please sign in to comment.