Skip to content

Commit

Permalink
feat(security): add option to check for secure node version
Browse files Browse the repository at this point in the history
  • Loading branch information
jegli committed Jun 4, 2021
2 parents 32d9ab5 + 9504086 commit 902ba3b
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 22 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ Feel free to use env-linter in a way that makes sense for your project. Here is

```json
{
"postinstall": "env-linter -h -s -d -vs 'node=12.x.x,npm=6.x.x'",
"prestart": "env-linter --versions 'node=12.x.x,npm=6.x.x'",
"postinstall": "env-linter -h -s -se -d -vs 'node=12.x.x,npm=6.x.x'",
"prestart": "env-linter -vs 'node=12.x.x,npm=6.x.x'",
"lint-staged": {
"**/package.json": ["env-linter --saveExact --dependenciesExactVersion"]
"**/package.json": ["env-linter -s -d"]
}
}
```
Expand Down Expand Up @@ -57,6 +57,10 @@ In any case, the used node version is compared to the list of [official node-rel

Checks if git-hooks are installed (i.e. husky installed). env-linter will stop any further process-execution if git-hooks are not installed.

### -se, --security

Checks if the used node version is considered secure according to the current list of node releases. If a newer node-version is available which was released due to a security concern, env-linter will stop any further process-execution. Find out more about the security-flag in this [github issue](https://github.com/nodejs/Release/issues/437).

### -s, --saveExact

Checks if the npm option `save-exact` is enabled, either through a .npmrc file in the project or in the user-directory. env-linter will stop any further process-execution if save-exact is disabled.
Expand Down
2 changes: 2 additions & 0 deletions __tests__/fetch-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('fetch-options', () => {
'--versions=node=12.14.0,npm=6.4.1',
'--hooksInstalled',
'--saveExact',
'--security',
'--dependenciesExactVersion',
'--lts',
];
Expand All @@ -20,6 +21,7 @@ describe('fetch-options', () => {
versions: ['node=12.14.0', 'npm=6.4.1'],
hooksInstalled: true,
saveExact: true,
security: true,
dependenciesExactVersion: true,
lts: true,
});
Expand Down
58 changes: 58 additions & 0 deletions __tests__/security.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import execa from 'execa';
import fetch from 'node-fetch';
import { getNodeSecurityChecker } from '../src/security';
import { logMessages } from '../src/log-messages';
/// <reference types="@types/jest" />

jest.mock('execa');
jest.mock('node-fetch');
jest.mock('fs-extra');


const { Response } = jest.requireActual('node-fetch');


const exampleNodeList = [
{ version: 'v13.0.0', date: '2020-07-29', npm: '6.14.7', security: true, lts: false },
{ version: 'v13.1.0', date: '2020-07-29', npm: '6.14.7', security: false, lts: false },
{ version: 'v13.2.0', date: '2020-07-29', npm: '6.14.7', security: false, lts: false },
{ version: 'v14.1.0', date: '2020-07-29', npm: '6.14.7', security: false, lts: false },
{ version: 'v14.2.0', date: '2020-07-29', npm: '6.14.7', security: false, lts: false },
{ version: 'v14.3.0', date: '2020-07-29', npm: '6.14.7', security: true, lts: false },
{ version: 'v15.1.0', date: '2020-07-29', npm: '6.14.7', security: false, lts: false },
{ version: 'v15.3.0', date: '2020-07-29', npm: '6.14.7', security: true, lts: false },
];
const nodeVersionListURL = 'https://nodejs.org/dist/index.json';

describe('getNodeSecurityChecker', () => {
it('should return success-text if there is no security version above inside this major version', async () => {
(execa as any).mockReturnValue(Promise.resolve({ stdout: '13.1.0' }));
(fetch as any).mockReturnValue(Promise.resolve(new Response(JSON.stringify(exampleNodeList))));
expect(await getNodeSecurityChecker()).toMatchObject({
error: false,
text: logMessages.success.nodeVersionSecurity('13.1.0'),
});
});
it('should return error-text if there is a security version above inside this major version', async () => {
(execa as any).mockReturnValue(Promise.resolve({ stdout: 'v14.1.0' }));
(fetch as any).mockReturnValue(Promise.resolve(new Response(JSON.stringify(exampleNodeList))));
expect(await getNodeSecurityChecker()).toMatchObject({
error: true,
text: logMessages.error.nodeVersionNotSecureError('14.1.0'),
});
});
it('should return warning-text if we can not receive node-list', async () => {
(fetch as any).mockReturnValue(Promise.reject());
expect(await getNodeSecurityChecker()).toMatchObject({
error: false,
text: logMessages.warning.fetchNodeListErrorNodeSecurity(nodeVersionListURL),
});
});
it('should return error-text if we can not receive installed node version', async () => {
(execa as any).mockReturnValue(Promise.reject());
expect(await getNodeSecurityChecker()).toMatchObject({
error: true,
text: logMessages.error.readProgramVersionError('node'),
});
});
});
5 changes: 5 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Security

This warning means, that your node version is not considered secure anymore. There has been a node release that fixes those vulnerabilities. Here is a [list of all node releases](https://nodejs.org/en/download/releases/) and here you likely find [more details to the release and the vulnerability](https://nodejs.org/en/blog/).

You need to update the node version of your project. You do **not** need to update to a new major version (e.g. from v14 to v16), a newer minor version is sufficient. Security releases are rolled out to all LTS versions of node. Here is an [example of such a security release](https://nodejs.org/en/blog/vulnerability/april-2021-security-releases/) which has rolled out to Node versions v10.x, v12.x, v14.x and v15.x.
17 changes: 12 additions & 5 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import chalk from 'chalk';
import { isCI } from 'ci-info';
import { getCwd } from './get-cwd';
import { getVersionCheckers } from './version-checker';
import { getSaveExactChecker } from './save-exact';
import { IOptions, ILogMessage } from './const';
import { getHooksInstalledChecker } from './hooks-installed';
import { splitVersions } from './fetch-options';
import { getExactDependencyVersionsChecker } from './exact-dependency-versions';
import { getHooksInstalledChecker } from './hooks-installed';
import { getNodeLTSChecker } from './lts';
import { getNodeSecurityChecker } from './security';
import { getSaveExactChecker } from './save-exact';
import { getVersionCheckers } from './version-checker';
import { ILogMessage, IOptions } from './const';
import { splitVersions } from './fetch-options';

export interface IApiOptions {
cwd?: string;
versions?: string[];
lts?: boolean;
security?: boolean;
hooksInstalled?: boolean;
saveExact?: boolean;
dependenciesExactVersion?: boolean;
Expand All @@ -24,6 +26,7 @@ export async function api(apiOptions: IApiOptions) {
cwd,
versions: splitVersions(apiOptions.versions),
lts: apiOptions.lts,
security: apiOptions.security,
hooksInstalled: apiOptions.hooksInstalled,
saveExact: apiOptions.saveExact,
dependenciesExactVersion: apiOptions.dependenciesExactVersion,
Expand All @@ -45,6 +48,10 @@ export async function api(apiOptions: IApiOptions) {
checkers.push(getNodeLTSChecker());
}

if (options.security) {
checkers.push(getNodeSecurityChecker());
}

if (options.hooksInstalled) {
checkers.push(getHooksInstalledChecker());
}
Expand Down
13 changes: 2 additions & 11 deletions src/const.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import { ProjectManifest } from '@pnpm/types';

// define cli api by using commander
export interface IOptions {
cwd?: string;
versions?: string[];
lts?: boolean;
security?: boolean;
hooksInstalled?: boolean;
saveExact?: boolean;
dependenciesExactVersion?: boolean;
}

export interface IProgram {
cwd?: string;
versions?: string;
hooksInstalled?: boolean;
saveExact?: boolean;
dependenciesExactVersion?: boolean;
lts?: boolean;
}

export interface INodeVersion {
version: string;
npm: string;
Expand Down Expand Up @@ -48,7 +39,7 @@ export interface IProject {

export type MarkdownDocsNames = keyof Pick<
IOptions,
'dependenciesExactVersion' | 'hooksInstalled' | 'lts' | 'saveExact' | 'versions'
'dependenciesExactVersion' | 'hooksInstalled' | 'lts' | 'saveExact' |'security' | 'versions'
>;

export type PackageDependencyKeys = 'dependencies' | 'devDependencies';
3 changes: 3 additions & 0 deletions src/fetch-node-versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export const getNodeList = async (purpose: string) => {
if (purpose === 'lts') {
return { error: true, text: logMessages.warning.fetchNodeListErrorNodeLTS(nodeVersionListURL) };
}
if (purpose === 'security') {
return { error: true, text: logMessages.warning.fetchNodeListErrorNodeSecurity(nodeVersionListURL) };
}
return { error: true, text: logMessages.warning.fetchNodeListErrorMatchingNPM(nodeVersionListURL) };
}
};
8 changes: 6 additions & 2 deletions src/fetch-options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from 'commander';
import { getCwd } from './get-cwd';
import { IProgram, IOptions } from './const';
import { IOptions } from './const';

export const splitVersions = (versions: string | string[] | undefined) => {
if (typeof versions === 'object') {
Expand All @@ -14,9 +14,10 @@ async function transformAnswersToOptions({
versions,
hooksInstalled,
saveExact,
security,
dependenciesExactVersion,
lts,
}: IProgram): Promise<IOptions> {
}: IOptions): Promise<IOptions> {
try {
const cwd = await getCwd();
const versionsSplit = splitVersions(versions);
Expand All @@ -25,6 +26,7 @@ async function transformAnswersToOptions({
versions: versionsSplit,
hooksInstalled,
saveExact,
security,
dependenciesExactVersion,
lts,
};
Expand All @@ -35,6 +37,7 @@ async function transformAnswersToOptions({
versions: undefined,
hooksInstalled: false,
saveExact: false,
security: false,
dependenciesExactVersion: false,
lts: false,
};
Expand All @@ -50,6 +53,7 @@ export async function fetchOptions(): Promise<IOptions> {
.option('-vs, --versions [string]', 'check versions of global packages eg. node, npm, ...')
.option('-h, --hooksInstalled', 'check if hooks are installed, failes if not')
.option('-s, --saveExact', 'check if npm save-exact is enabled, failes if not')
.option('-se, --security', 'check if node version is secure, failes if not')
.option('-d, --dependenciesExactVersion', 'check if dependencies are installed as an exact version')
.option('-l, --lts', 'check if the used node version is LTS');

Expand Down
12 changes: 11 additions & 1 deletion src/log-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const logMessages = {
),
nodeVersionLTS: (usedNodeVersion: string) =>
chalk.green(`${logSymbols.success} Your node version ${usedNodeVersion} is a LTS version.`),
nodeVersionSecurity: (usedNodeVersion: string) =>
chalk.green(`${logSymbols.success} Your minor node version ${usedNodeVersion} is a secure version.`),
programVersionSatisfies: (program: string, usedVersion: string, expectedVersion: string) =>
chalk.green(
`${logSymbols.success} Your ${program} version ${usedVersion} works with the required version (${expectedVersion}) of your project.`
Expand All @@ -39,6 +41,10 @@ export const logMessages = {
'lts'
)}`
),
nodeVersionNotSecureError: (usedNodeVersion: string) =>
chalk.red(
`${logSymbols.error} Change node-version! There is a security update for your used major version. You are using node ${usedNodeVersion} which is not considered secure. ${createTerminalLink('security')}`
),
wrongNPMVersionError: (usedNodeVersion: string, usedNPMVersion: string) =>
chalk.red(
`${
Expand Down Expand Up @@ -85,7 +91,7 @@ export const logMessages = {
),
gitHooksNotInstalledError: () =>
chalk.red(
`${logSymbols.error} Git hooks are not installed. Install with "npm i -D husky". ${createTerminalLink(
`${logSymbols.error} Git hooks are not installed. ${createTerminalLink(
'hooksInstalled'
)}`
),
Expand Down Expand Up @@ -119,5 +125,9 @@ export const logMessages = {
chalk.yellow(
`${logSymbols.warning} Could not fetch node-list from ${nodeVersionListURL}. Your node version might not be a LTS version.`
),
fetchNodeListErrorNodeSecurity: (nodeVersionListURL: string) =>
chalk.yellow(
`${logSymbols.warning} Could not fetch node-list from ${nodeVersionListURL}. Your node version might not be a secure node version.`
),
},
};
33 changes: 33 additions & 0 deletions src/security.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import semver from 'semver';
import { getInstalledVersion } from './get-version';
import { getNodeList } from './fetch-node-versions';
import { ILogMessage, INodeVersion } from './const';
import { logMessages } from './log-messages';

export const hasNodeVersionSecurityIssues = (nodeList: INodeVersion[], usedNodeVersion: string) => {
return nodeList.some((nodeVersion) => {
const nodeVersionText = nodeVersion.version.slice(1);
// return true, if any MINOR version above THIS version has a security flag
return (
semver.diff(nodeVersionText, usedNodeVersion) === 'minor' &&
semver.gt(nodeVersionText, usedNodeVersion) &&
nodeVersion.security
);
});
};

export const getSecurityNodeLog = async (usedNodeVersion: string) => {
const nodeList = await getNodeList('security');
if (nodeList.error) {
return { error: false, text: nodeList.text };
}

return hasNodeVersionSecurityIssues(JSON.parse(nodeList.text), usedNodeVersion)
? { error: true, text: logMessages.error.nodeVersionNotSecureError(usedNodeVersion) }
: { error: false, text: logMessages.success.nodeVersionSecurity(usedNodeVersion) };
};

export const getNodeSecurityChecker = async () => {
const usedNodeVersion: ILogMessage = await getInstalledVersion('node');
return usedNodeVersion.error ? usedNodeVersion : getSecurityNodeLog(usedNodeVersion.text);
};

0 comments on commit 902ba3b

Please sign in to comment.