Skip to content

Commit

Permalink
feat: add snyk apps create command
Browse files Browse the repository at this point in the history
  • Loading branch information
love-bhardwaj committed Jan 31, 2022
1 parent 24b6c22 commit b6fb192
Show file tree
Hide file tree
Showing 22 changed files with 1,043 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -13,6 +13,8 @@ help/ @snyk/hammer
src/cli/commands/test/iac-local-execution/ @snyk/group-infrastructure-as-code
src/cli/commands/test/iac-output.ts @snyk/group-infrastructure-as-code
src/cli/commands/test/iac-test-shim.ts @snyk/group-infrastructure-as-code
src/cli/commands/apps @snyk/moose
src/lib/apps @snyk/moose
src/lib/cloud-config-projects.ts @snyk/group-infrastructure-as-code
src/lib/plugins/sast/ @snyk/sast-team
test/fixtures/sast/ @snyk/sast-team
Expand All @@ -32,6 +34,7 @@ test/smoke/.iac-data/ @snyk/group-infrastructure-as-code
test/jest/unit/lib/formatters/iac-output.spec.ts @snyk/group-infrastructure-as-code
test/jest/unit/iac-unit-tests/ @snyk/group-infrastructure-as-code
test/jest/acceptance/iac/ @snyk/group-infrastructure-as-code
test/jest/acceptance/snyk-apps @snyk-moose
src/lib/errors/invalid-iac-file.ts @snyk/group-infrastructure-as-code
src/lib/errors/unsupported-options-iac-error.ts @snyk/group-infrastructure-as-code
src/lib/errors/no-supported-sast-files-found.ts @snyk/sast-team
Expand Down
1 change: 1 addition & 0 deletions config.default.json
@@ -1,5 +1,6 @@
{
"API": "https://snyk.io/api/v1",
"API_V3_URL": "https://api.snyk.io/v3",
"devDeps": false,
"PRUNE_DEPS_THRESHOLD": 40000,
"MAX_PATH_COUNT": 1500000,
Expand Down
55 changes: 55 additions & 0 deletions help/cli-commands/apps.md
@@ -0,0 +1,55 @@
# snyk apps -- Create and manage your Snyk Apps

# Usage

`snyk apps <COMMAND> [<OPTIONS>]`

## Description

Snyk Apps are integrations that extend the functionality of the Snyk platform. They provide you with an opportunity to mould your Snyk experience to suit your specific needs.

[For more information see our user docs](https://docs.snyk.io/features/integrations/snyk-apps)

## Commands

**_Note: All `apps` commands are only accessible behind the `--experimental` flag and the behaviour can change at any time, without prior notice. You are kindly advised to use all the commands with caution_**

### `create`

Create a new Snyk App.

## Options

### `--interactive`

Use the command in interactive mode.

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

(Required for the `create` command)
Specify the `<ORG_ID>` to create the Snyk App under.

### `--name=<SNYK_APP_NAME>`

(Required for the `create` command)
The name of Snyk App that will be displayed to the user during the authentication flow.

### `--redirect-uris=<REDIRECT_URIS>`

(Required for the `create` command)
A comma separated list of redirect URIs. This will form a list of allowed redirect URIs to call back after authentication.

### `--scopes=<SCOPES>`

(Required for the `create` command)
A comma separated list of scopes required by your Snyk App. This will for a list of scopes that your app is allowed to request during authorization.

## Examples

### `Create Snyk App`

\$ snyk apps create --experimental --org=48ebb069-472f-40f4-b5bf-d2d103bc02d4 --name='My Awesome App' --redirect-uris=https://example1.com,https://example2.com --scopes=apps:beta

### `Create Snyk App Interactive Mode`

\$ snyk apps create --experimental --interactive
57 changes: 57 additions & 0 deletions src/cli/commands/apps/create-app.ts
@@ -0,0 +1,57 @@
import * as Debug from 'debug';
import {
EAppsURL,
getAppsURL,
handleCreateAppRes,
handleV3Error,
ICreateAppRequest,
ICreateAppResponse,
SNYK_APP_DEBUG,
} from '../../../lib/apps';
import { makeRequestV3 } from '../../../lib/request/promise';
import { spinner } from '../../../lib/spinner';

const debug = Debug(SNYK_APP_DEBUG);

/**
* Function to process the app creation request and
* handle any errors that are request error and print
* in a formatted string. It throws is error is unknown
* or cannot be handled.
* @param {ICreateAppRequest} data to create the app
* @returns {String} response formatted string
*/
export async function createApp(
data: ICreateAppRequest,
): Promise<string | void> {
debug('App data', data);
const {
orgId,
snykAppName: name,
snykAppRedirectUris: redirectUris,
snykAppScopes: scopes,
} = data;
const payload = {
method: 'POST',
url: getAppsURL(EAppsURL.CREATE_APP, { orgId }),
body: {
name,
redirectUris,
scopes,
},
qs: {
version: '2021-08-11~experimental',
},
};

try {
await spinner('Creating your Snyk App');
const response = await makeRequestV3<ICreateAppResponse>(payload);
debug(response);
spinner.clearAll();
return handleCreateAppRes(response);
} catch (error) {
spinner.clearAll();
handleV3Error(error);
}
}
47 changes: 47 additions & 0 deletions src/cli/commands/apps/index.ts
@@ -0,0 +1,47 @@
import * as Debug from 'debug';
import { MethodArgs } from '../../args';
import { processCommandArgs } from '../process-command-args';
import {
EValidSubCommands,
validAppsSubCommands,
SNYK_APP_DEBUG,
ICreateAppOptions,
AppsErrorMessages,
} from '../../../lib/apps';

import { createApp } from './create-app';
// import * as path from 'path';
import {
createAppDataInteractive,
createAppDataScriptable,
} from '../../../lib/apps/create-app';
import help from '../help';

const debug = Debug(SNYK_APP_DEBUG);

export default async function apps(
...args0: MethodArgs
): Promise<string | undefined | any> {
debug('Snyk apps CLI called');

const { options, paths } = processCommandArgs<ICreateAppOptions>(...args0);
debug(options, paths);

const commandVerb1 = paths[0];
const validCommandVerb =
commandVerb1 && validAppsSubCommands.includes(commandVerb1);
if (!validCommandVerb) {
// Display help md for apps
debug(`Unknown subcommand: ${commandVerb1}`);
return help('apps');
}
// Check if experimental flag is being used
if (!options.experimental) throw new Error(AppsErrorMessages.useExperimental);

if (commandVerb1 === EValidSubCommands.CREATE) {
const createAppData = options.interactive
? await createAppDataInteractive()
: createAppDataScriptable(options);
return await createApp(createAppData);
}
}
1 change: 1 addition & 0 deletions src/cli/commands/index.js
Expand Up @@ -20,6 +20,7 @@ const commands = {
wizard: async (...args) => callModule(import('./protect/wizard'), args),
woof: async (...args) => callModule(import('./woof'), args),
log4shell: async (...args) => callModule(import('./log4shell'), args),
apps: async (...args) => callModule(import('./apps'), args),
};

commands.aliases = abbrev(Object.keys(commands));
Expand Down
57 changes: 57 additions & 0 deletions src/lib/apps/constants.ts
@@ -0,0 +1,57 @@
import chalk from 'chalk';

export const SNYK_APP_NAME = 'snykAppName';
export const SNYK_APP_REDIRECT_URIS = 'snykAppRedirectUris';
export const SNYK_APP_SCOPES = 'snykAppScopes';
export const SNYK_APP_CLIENT_ID = 'snykAppClientId';
export const SNYK_APP_ORG_ID = 'snykAppOrgId';
export const SNYK_APP_DEBUG = 'snyk:apps';

export enum EValidSubCommands {
CREATE = 'create',
}

export enum EAppsURL {
CREATE_APP,
}

export const validAppsSubCommands = Object.values<string>(EValidSubCommands);

export const AppsErrorMessages = {
orgRequired: `Option '--org' is required! For interactive mode, please use '--interactive' or '-i' flag. For more information please run the help command 'snyk apps --help' or 'snyk apps -h'.`,
nameRequired: `Option '--name' is required! For interactive mode, please use '--interactive' or '-i' flag. For more information please run the help command 'snyk apps --help' or 'snyk apps -h'.`,
redirectUrisRequired: `Option '--redirect-uris' is required! For interactive mode, please use '--interactive' or '-i' flag. For more information please run the help command 'snyk apps --help' or 'snyk apps -h'.`,
scopesRequired: `Option '--scopes' is required! For interactive mode, please use '--interactive' or '-i' flag. For more information please run the help command 'snyk apps --help' or 'snyk apps -h'.`,
useExperimental: `\n${chalk.redBright(
"All 'apps' commands are only accessible behind the '--experimental' flag.",
)}\n
The behaviour can change at any time, without prior notice.
You are kindly advised to use all the commands with caution.
${chalk.bold('Usage')}
${chalk.italic('snyk apps <COMMAND> --experimental')}\n`,
};

export const CreateAppPromptData = {
SNYK_APP_NAME: {
name: SNYK_APP_NAME,
message: `Name of the Snyk App (visible to users when they install the Snyk App)?`,
},
SNYK_APP_REDIRECT_URIS: {
name: SNYK_APP_REDIRECT_URIS,
message: `Your Snyk App's redirect URIs (comma seprated list. ${chalk.yellowBright(
' Ex: https://example1.com,https://example2.com',
)})?: `,
},
SNYK_APP_SCOPES: {
name: SNYK_APP_SCOPES,
message: `Your Snyk App's permission scopes (comma separated list. ${chalk.yellowBright(
' Ex: apps:beta',
)})?: `,
},
SNYK_APP_ORG_ID: {
name: SNYK_APP_ORG_ID,
message:
'Please provide the org id under which you want to create your Snyk App: ',
},
};
73 changes: 73 additions & 0 deletions src/lib/apps/create-app/index.ts
@@ -0,0 +1,73 @@
import {
AppsErrorMessages,
createAppPrompts,
ICreateAppRequest,
ICreateAppOptions,
SNYK_APP_NAME,
SNYK_APP_REDIRECT_URIS,
SNYK_APP_SCOPES,
SNYK_APP_ORG_ID,
validateUUID,
validateAllURL,
} from '..';
import * as inquirer from '@snyk/inquirer';
import { ValidationError } from '../../errors';

/**
* Validates and parsed the data required to create app.
* Throws error if option is not provided or is invalid
* @param {ICreateAppOptions} options required to create an app
* @returns {ICreateAppRequest} data that is used to make the request
*/
export function createAppDataScriptable(
options: ICreateAppOptions,
): ICreateAppRequest {
if (!options.org) {
throw new ValidationError(AppsErrorMessages.orgRequired);
} else if (typeof validateUUID(options.org) === 'string') {
// Combines to form "Invalid UUID provided for org id"
throw new ValidationError(`${validateUUID(options.org)} for org id`);
} else if (!options.name) {
throw new ValidationError(AppsErrorMessages.nameRequired);
} else if (!options['redirect-uris']) {
throw new ValidationError(AppsErrorMessages.redirectUrisRequired);
} else if (typeof validateAllURL(options['redirect-uris']) === 'string') {
throw new ValidationError(
validateAllURL(options['redirect-uris']) as string,
);
} else if (!options.scopes) {
throw new ValidationError(AppsErrorMessages.scopesRequired);
} else {
return {
orgId: options.org,
snykAppName: options.name,
snykAppRedirectUris: options['redirect-uris']
.replace(/\s+/g, '')
.split(','),
snykAppScopes: options.scopes.replace(/\s+/g, '').split(','),
};
}
}

// Interactive format
export async function createAppDataInteractive(): Promise<ICreateAppRequest> {
// Proceed with interactive
const answers = await inquirer.prompt(createAppPrompts);
// Process answers
const snykAppName = answers[SNYK_APP_NAME].trim() as string;
const snykAppRedirectUris = answers[SNYK_APP_REDIRECT_URIS].replace(
/\s+/g,
'',
).split(',') as string[];
const snykAppScopes = answers[SNYK_APP_SCOPES].replace(/\s+/g, '').split(
',',
) as string[];
const orgId = answers[SNYK_APP_ORG_ID].trim() as string;
// POST: to create an app
return {
orgId,
snykAppName,
snykAppRedirectUris,
snykAppScopes,
};
}
6 changes: 6 additions & 0 deletions src/lib/apps/index.ts
@@ -0,0 +1,6 @@
export * from './constants';
export * from './prompts';
export * from './types';
export * from './v3-utils';
export * from './utils';
export * from './input-validator';
60 changes: 60 additions & 0 deletions src/lib/apps/input-validator.ts
@@ -0,0 +1,60 @@
import * as uuid from 'uuid';

/**
*
* @param {String} input of space separated URL/URI passed by
* user for redirect URIs
* @returns { String | Boolean } complying with inquirer return values, the function
* separates the string on space and validates each to see
* if a valid URL/URI. Return a string if invalid and
* boolean true if valid
*/
export function validateAllURL(input: string): string | boolean {
const trimmedInput = input.trim();
let errMessage = '';
for (const i of trimmedInput.split(',')) {
if (typeof validURL(i) == 'string')
errMessage = errMessage + `\n${validURL(i)}`;
}

if (errMessage) return errMessage;
return true;
}

/**
* Custom validation logic which takes in consideration
* creation of Snyk Apps and thus allows localhost.com
* as a valid URL.
* @param {String} input of URI/URL value to validate using
* regex
* @returns {String | Boolean } string message is not valid
* and boolean true if valid
*/
export function validURL(input: string): boolean | string {
try {
new URL(input);
return true;
} catch (error) {
return `${input} is not a valid URL`;
}
}

/**
* Function validates if a valid UUID (version of UUID not tacken into account)
* @param {String} input UUID to be validated
* @returns {String | Boolean } string message is not valid
* and boolean true if valid
*/
export function validateUUID(input: string): boolean | string {
return uuid.validate(input) ? true : 'Invalid UUID provided';
}

/**
* @param {String} input
* @returns {String | Boolean } string message is not valid
* and boolean true if valid
*/
export function validInput(input: string): string | boolean {
if (!input) return 'Please enter something';
return true;
}

0 comments on commit b6fb192

Please sign in to comment.