Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MVP of CLI to file PRs with Package Updates #1128

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .idea/dbnavigator.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

DROP TABLE IF EXISTS vulnerability.cisa_known_exploited CASCADE;
DROP INDEX IF EXISTS vulnerability_equivalent_b_idx;
DROP FUNCTION vulnerability.cisa_known_exploited_vulnerability;
DROP FUNCTION vulnerability.vulnerability_cisa_known_exploited;
DROP FUNCTION IF EXISTS vulnerability.cisa_known_exploited_vulnerability;
DROP FUNCTION IF EXISTS vulnerability.vulnerability_cisa_known_exploited;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ ALTER TABLE vulnerability.vulnerability ADD COLUMN cve_id text NULL DEFAULT NULL



DROP FUNCTION vulnerability.cisa_known_exploited_vulnerability;
DROP FUNCTION IF EXISTS vulnerability.cisa_known_exploited_vulnerability;

DROP FUNCTION vulnerability.vulnerability_cisa_known_exploited
DROP FUNCTION IF EXISTS vulnerability.vulnerability_cisa_known_exploited
6 changes: 6 additions & 0 deletions lunatrace/npm-package-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@
"@oclif/core": "^2",
"@oclif/plugin-help": "^5",
"@oclif/plugin-plugins": "^2.2.4",
"@octokit/core": "^4.2.0",
"@octokit/plugin-paginate-rest": "^6.0.0",
"@octokit/rest": "^19.0.7",
"npm-package-arg": "^10.1.0",
"octokit-plugin-create-pull-request": "^4.1.1",
"pacote": "^15.0.8"
},
"devDependencies": {
"@oclif/test": "^2.3.3",
"@octokit/plugin-rest-endpoint-methods": "^7.0.1",
"@octokit/types": "^9.0.0",
"@types/chai": "^4",
"@types/jest": "^29.4.0",
"@types/node": "^16.18.11",
Expand Down
110 changes: 110 additions & 0 deletions lunatrace/npm-package-cli/src/commands/github-pr/replace-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2023 by LunaSec (owned by Refinery Labs, Inc)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { Args, Command, Flags } from '@oclif/core';

import {
PullRequestOctokit,
PullRequestOctokitType,
replacePackageAndFileGitHubPullRequest,
} from '../../package/github-pr';
import { PackageManagerType } from '../../package/types';
import { ReplacePackageFlags } from '../replace-package';

export default class GitHubReplacePackage extends Command {
static description = 'Replaces a package in an NPM project on GitHub and then files a Pull Request';

static examples = [
`$ lunatrace-npm-cli github-pr replace-package owner/repo --githubToken <SECRET> --old js-yaml@^3.13.1 --new js-yaml@^3.14.0`,
];

static flags = {
...ReplacePackageFlags,
githubToken: Flags.string({
aliases: ['github-token'],
env: 'GITHUB_TOKEN',
required: true,
summary: 'GitHub Token to authenticate against the API with.',
}),
gitRef: Flags.string({
aliases: ['ref'],
required: false,
summary: 'Optional git ref to use for the pull request',
}),
manifestPath: Flags.string({
aliases: ['manifest-path'],
default: '/',
required: false,
summary: 'Path in the repo to the folder containing manifest files.',
}),
packageManager: Flags.string({
aliases: ['package-manager'],
default: 'npm',
options: ['npm', 'yarn'],
required: false,
summary: `Package manager to read files from the repo and modify.
If this is npm, it will read 'package-lock.json' and for Yarn 'yarn.lock'.`,
}),
tempWritePath: Flags.string({
aliases: ['temp-write-path'],
required: false,
summary: 'The folder where the temporary files will be downloaded to and written.',
}),
};

static args = {
repo: Args.string({
description: 'Repo to read package data from. Specified as <:user>/<:repo> (like lunasec-io/lunasec).',
required: true,
}),
};

async run(): Promise<void> {
const { args, flags } = await this.parse(GitHubReplacePackage);

const splitRepo = args.repo.split('/');

if (splitRepo.length !== 2) {
throw new Error(`Invalid repo passed. Must be <:user>/<:repo> (like lunasec-io/lunasec)`);
}

const userOrOrg = splitRepo[0];
const repo = splitRepo[1];

if (!userOrOrg) {
throw new Error('Missing user or org for command');
}

if (!repo) {
throw new Error('Missing repo for command');
}

const octokit = new PullRequestOctokit({
auth: flags.githubToken,
});

await replacePackageAndFileGitHubPullRequest(
octokit,
userOrOrg,
repo,
flags.manifestPath,
flags.packageManager as PackageManagerType,
flags.old,
flags.new,
flags.gitRef
);
}
}
57 changes: 17 additions & 40 deletions lunatrace/npm-package-cli/src/commands/replace-package/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,28 @@
* limitations under the License.
*
*/
import Arborist from '@npmcli/arborist';
import { Args, Command, Flags } from '@oclif/core';
import npa from 'npm-package-arg';
import { manifest } from 'pacote';

import { setupPackageTree } from '../../package/package-tree';
import { replacePackagesForNode } from '../../package/replace-package';
import { setupPackageTree } from '../../package/replace-package/package-tree';
import { getScriptPath } from '../../package/utils/get-script-path';

export const ReplacePackageFlags = {
old: Flags.string({ description: 'Target Package with Semver range to remove from Package.json', required: true }),
new: Flags.string({
description: 'New Package with Semver range to use as replacement in Package.json',
required: true,
}),
};

export default class ReplacePackage extends Command {
static description = 'Prints an NPM package tree';
static description = 'Replaces an NPM package in the package tree';

static examples = [
`$ lunatrace-npm-cli show-tree /path/to/node/project
`,
`$ lunatrace-npm-cli replace-package /path/to/node/project --old react@^16.0.0 --new react@^16.2.0`,
];

static flags = {
old: Flags.string({ description: 'Target Package with Semver range to remove from Package.json', required: true }),
new: Flags.string({
description: 'New Package with Semver range to use as replacement in Package.json',
required: true,
}),
};
static flags = ReplacePackageFlags;

static args = {
root: Args.string({ description: 'Root folder to read package.json from', required: false }),
Expand All @@ -54,38 +53,16 @@ export default class ReplacePackage extends Command {
});

// TODO: Figure out why Arborist marks everything as "extraneous" in the generated lockfile.
const node = await tree.loadVirtualTreeFromRoot();

const oldPackage = npa(flags.old);

// TODO: Figure out if this works for `git` packages as well. (It probably doesn't and will require a separate code path)
const nodes = await node.querySelectorAll(`[name=${oldPackage.escapedName}]:semver(${oldPackage.rawSpec})`);
const node = await tree.arborist.loadVirtual();

const resolvedManifest = await manifest(flags.new);

nodes.map((n) => {
if (!n.parent) {
throw new Error('Unable to remove package for node without a parent');
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
n.package = {
...resolvedManifest,
resolved: resolvedManifest._resolved,
integrity: resolvedManifest._integrity,
};

// These fields are required by Arborist to properly update the lockfile.
n.resolved = resolvedManifest._resolved;
n.integrity = resolvedManifest._integrity;
});
const { updatedNodes } = await replacePackagesForNode(node, flags.old, flags.new);

this.log(`Updated ${nodes.length} packages`);
this.log(`Updated ${updatedNodes.length} packages`);

// This updates the package-lock.json file on disk.
// Note: We may actually need to call `tree.reify()` in order to get the transitive dependencies to update.
// It's unclear and untested currently.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await node.meta.save();

Expand Down
4 changes: 2 additions & 2 deletions lunatrace/npm-package-cli/src/commands/show-tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { join } from 'path';
import Arborist from '@npmcli/arborist';
import { Args, Command } from '@oclif/core';

import { setupPackageTree } from '../../package/package-tree';
import { setupPackageTree } from '../../package/replace-package/package-tree';
import { getScriptPath } from '../../package/utils/get-script-path';

export default class ShowTree extends Command {
Expand All @@ -45,7 +45,7 @@ export default class ShowTree extends Command {
root: root,
});

const virtualTree = await tree.loadVirtualTreeFromRoot();
const virtualTree = await tree.arborist.loadVirtual();

this.log('Package tree:\n');

Expand Down
Loading