Skip to content
Open
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
56 changes: 56 additions & 0 deletions cliv2/pkg/core/help_docs_audit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package core

import (
"encoding/json"
"fmt"
"os"
"sort"
"testing"

"github.com/snyk/go-application-framework/pkg/configuration"
"github.com/snyk/go-application-framework/pkg/workflow"
"github.com/stretchr/testify/require"
)

type registeredCommandForHelpAudit struct {
Command string `json:"command"`
Visible bool `json:"visible"`
}

func TestPrintRegisteredCommandTreeForHelpAudit(t *testing.T) {
if os.Getenv("SNYK_HELP_AUDIT_PRINT_COMMANDS") != "1" {
t.Skip("set SNYK_HELP_AUDIT_PRINT_COMMANDS=1 to print registered commands")
}

config := configuration.New()
engine := workflow.NewWorkFlowEngine(config)
initExtensions(engine, config, nil)
require.NoError(t, engine.Init())

commands := []registeredCommandForHelpAudit{}
for _, workflowID := range engine.GetWorkflows() {
command := workflow.GetCommandFromWorkflowIdentifier(workflowID)
if command == "" {
continue
}

entry, ok := engine.GetWorkflow(workflowID)
if !ok {
continue
}

commands = append(commands, registeredCommandForHelpAudit{
Command: command,
Visible: entry.IsVisible(),
})
}

sort.Slice(commands, func(i, j int) bool {
return commands[i].Command < commands[j].Command
})

output, err := json.Marshal(commands)
require.NoError(t, err)

fmt.Printf("SNYK_HELP_AUDIT_COMMANDS=%s\n", output)
}
61 changes: 61 additions & 0 deletions help/cli-commands/ignore-create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Ignore create

## Usage

`snyk ignore create [<OPTIONS>]`

## Description

The `snyk ignore create` command creates an ignore request for a finding in a Snyk Organization.

This command creates an ignore request through the Snyk ignore workflow. It does not edit the local `.snyk` policy file. To add ignores to a local policy file, use the `snyk ignore` command.

In interactive mode, the command prompts for missing values. In non-interactive mode, provide the required options.

## Options

### `--finding-id=<FINDING_ID>`

The ID of the finding to ignore. Required when `--interactive=false`.

### `--ignore-type=<IGNORE_TYPE>`

The ignore type to create. Required when `--interactive=false`.

Supported values:

- `not-vulnerable`
- `wont-fix`
- `temporary-ignore`

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

The reason for the ignore. Required when `--interactive=false`.

### `--expiration=<EXPIRATION>`

The ignore expiration date. Use `YYYY-MM-DD` format, or `never` for no expiration. Required when `--interactive=false`.

### `--remote-repo-url=<URL>`

The remote repository URL for the project. The command detects this value automatically when possible.

### `--interactive=<true|false>`

Run the command in interactive mode.

Default: `true`

### `--org=<ORG_ID>`

Specify the Snyk Organization ID to use for the ignore request. The value must be an Organization UUID.

## Examples

Create an ignore request interactively:

`$ snyk ignore create`

Create a temporary ignore request without prompts:

`$ snyk ignore create --finding-id=<FINDING_ID> --ignore-type=temporary-ignore --reason='Temporarily accepted risk' --expiration=2026-07-01 --interactive=false`
4 changes: 4 additions & 0 deletions help/cli-commands/ignore.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ The `snyk ignore` command modifies the `.snyk` policy file to ignore a specified

**Note:** Ignoring issues or vulnerabilities using the `.snyk` file is not supported for Snyk Code.

### Create ignore requests

[`snyk ignore create`](ignore-create.md); `snyk ignore create --help`: creates an ignore request for a finding.

### Exclude

`snyk ignore [--expiry=] [--reason=] [--policy-path=<PATH_TO_POLICY_FILE>] [--file-path=<PATH_TO_RESOURCE>] [OPTIONS]`
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"clean": "npx rimraf node_modules dist binary-releases test-results *.tgz tsconfig.tsbuildinfo .eslintcache pysrc packages/*/node_modules packages/*/dist packages/*/tsconfig.tsbuildinfo packages/*/*.tgz",
"format": "prettier --write '**/*.{js,ts,json,yaml,yml,md}' && npm run lint:js -- --fix",
"format:changes": "./scripts/format/prettier-changes.sh",
"help:audit": "node scripts/audit-cli-help-docs.js",
"lint": "npm-run-all --serial --continue-on-error lint:*",
"lint:js": "eslint --quiet --color --cache '**/*.{js,ts}'",
"lint:formatting": "prettier --check '**/*.{js,ts,json,yaml,yml,md}'",
Expand Down
167 changes: 167 additions & 0 deletions scripts/audit-cli-help-docs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env node

const { spawnSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const repoRoot = path.resolve(__dirname, '..');
const helpDir = path.join(repoRoot, 'help', 'cli-commands');
const failOnMissing = process.argv.includes('--fail-on-missing');
const includeHidden = process.argv.includes('--include-hidden');
const includeInternal = process.argv.includes('--include-internal');

const internalCommands = new Set([
'datatransformation',
'filter',
'help',
'internal cleanup',
'legacycli',
'output',
'reportanalytics',
]);

function readFile(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
}

function expectedHelpFile(command) {
return `${command.replace(/\s+/g, '-')}.md`;
}

function addCommand(commands, command, source, visible = true) {
const normalizedCommand = command.trim().replace(/\s+/g, ' ');
if (!normalizedCommand) {
return;
}

if (!commands.has(normalizedCommand)) {
commands.set(normalizedCommand, {
command: normalizedCommand,
sources: new Set(),
visible,
});
}

const entry = commands.get(normalizedCommand);
entry.sources.add(source);
entry.visible = entry.visible || visible;
}

function legacyCommands() {
const commands = new Map();
const commandIndex = readFile('src/cli/commands/index.js');

for (const match of commandIndex.matchAll(
/^\s*(?:'([^']+)'|"([^"]+)"|([a-zA-Z][\w-]*)):\s*async/gm,
)) {
addCommand(commands, match[1] || match[2] || match[3], 'legacy command');
}

const modes = readFile('src/cli/modes.ts');
for (const match of modes.matchAll(
/^\s{2}([a-zA-Z][\w-]*):\s*{\s*allowedCommands:\s*\[([^\]]*)\]/gm,
)) {
const mode = match[1];
const allowedCommands = Array.from(
match[2].matchAll(/'([^']+)'|"([^"]+)"/g),
(allowedCommandMatch) => allowedCommandMatch[1] || allowedCommandMatch[2],
);

addCommand(commands, mode, 'legacy mode');
for (const allowedCommand of allowedCommands) {
addCommand(commands, `${mode} ${allowedCommand}`, 'legacy mode');
}
}

return commands;
}

function nativeCommands() {
const result = spawnSync(
'go',
[
'test',
'./pkg/core',
'-run',
'TestPrintRegisteredCommandTreeForHelpAudit',
'-count=1',
'-v',
],
{
cwd: path.join(repoRoot, 'cliv2'),
encoding: 'utf8',
env: {
...process.env,
SNYK_HELP_AUDIT_PRINT_COMMANDS: '1',
},
},
);

if (result.status !== 0) {
process.stderr.write(result.stdout);
process.stderr.write(result.stderr);
process.exit(result.status || 1);
}

const match = result.stdout.match(/SNYK_HELP_AUDIT_COMMANDS=(\[.*\])/);
if (!match) {
process.stderr.write(result.stdout);
process.stderr.write(result.stderr);
throw new Error('Could not find registered command audit output.');
}

return JSON.parse(match[1]);
}

function helpFiles() {
return new Set(
fs
.readdirSync(helpDir)
.filter(
(fileName) => fileName.endsWith('.md') && fileName !== 'README.md',
),
);
}

function collectCommands() {
const commands = legacyCommands();

for (const nativeCommand of nativeCommands()) {
addCommand(
commands,
nativeCommand.command,
nativeCommand.visible ? 'native workflow' : 'native workflow hidden',
nativeCommand.visible,
);
}

return Array.from(commands.values()).map((entry) => ({
...entry,
sources: Array.from(entry.sources).sort(),
}));
}

const docs = helpFiles();
const missing = collectCommands()
.filter((entry) => includeInternal || !internalCommands.has(entry.command))
.filter((entry) => includeHidden || entry.visible)
.filter((entry) => !docs.has(expectedHelpFile(entry.command)))
.sort((a, b) => a.command.localeCompare(b.command));

if (missing.length === 0) {
console.log('All discovered CLI commands have help docs.');
process.exit(0);
}

console.log('CLI commands without help docs:');
for (const entry of missing) {
console.log(
`- snyk ${entry.command} -> help/cli-commands/${expectedHelpFile(
entry.command,
)} (${entry.sources.join(', ')})`,
);
}

if (failOnMissing) {
process.exit(1);
}
17 changes: 17 additions & 0 deletions test/jest/acceptance/help.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ describe('help', () => {
expect(stderr).toBe('');
});

it('prints specific help info for ignore', async () => {
const { stdout, code, stderr } = await runSnykCLI('ignore --help');

expect(stdout).toContain('snyk ignore create');
expect(code).toBe(0);
expect(stderr).toBe('');
});

it('prints specific help info for ignore create', async () => {
const { stdout, code, stderr } = await runSnykCLI('ignore create --help');

expect(stdout).toContain('Ignore create');
expect(stdout).toContain('snyk ignore create [<OPTIONS>]');
expect(code).toBe(0);
expect(stderr).toBe('');
});

it('prints specific help info when called with flag and equals sign', async () => {
const { stdout, code, stderr } = await runSnykCLI('--help=iac');

Expand Down
Loading