diff --git a/.github/workflows/lunatrace-stack-and-hasura.yaml b/.github/workflows/lunatrace-stack-and-hasura.yaml index b0d9df9b3..4dfadc851 100644 --- a/.github/workflows/lunatrace-stack-and-hasura.yaml +++ b/.github/workflows/lunatrace-stack-and-hasura.yaml @@ -36,6 +36,7 @@ jobs: - name: Build run: |- set -x + (cd lunatrace/npm-package-cli && yarn run compile) (cd lunatrace/bsl/logger && yarn run compile) (cd lunatrace/bsl/common && yarn run compile) (cd lunatrace/bsl/backend && yarn run compile) diff --git a/lunatrace/bsl/backend/src/graphql-yoga/resolvers/create-pull-request-for-vulnerability.ts b/lunatrace/bsl/backend/src/graphql-yoga/resolvers/create-pull-request-for-vulnerability.ts index 743589d98..326ad7020 100644 --- a/lunatrace/bsl/backend/src/graphql-yoga/resolvers/create-pull-request-for-vulnerability.ts +++ b/lunatrace/bsl/backend/src/graphql-yoga/resolvers/create-pull-request-for-vulnerability.ts @@ -15,7 +15,7 @@ import { GraphQLYogaError } from '@graphql-yoga/node'; import { PullRequestOctokit, replacePackageAndFileGitHubPullRequest, -} from '@lunatrace/npm-package-cli/src/package/github-pr'; +} from '@lunatrace/npm-package-cli/src/package/github-pr/index'; import { getInstallationAccessToken } from '../../github/auth'; import { hasura } from '../../hasura-api'; @@ -26,6 +26,7 @@ import { checkProjectIsAuthorizedOrThrow, isAuthenticated } from '../helpers/aut type CreatePullRequestForVulnerabilityType = NonNullable; function splitGitHubRepoPath(gitHubRepo: string) { + // URL from GitHub of the repo, eg: git://github.com/lunasec-io/lunasec.git const split = gitHubRepo.split('/'); const owner = split[split.length - 2]; const repo = split[split.length - 1].replace('.git', ''); diff --git a/lunatrace/bsl/frontend/src/api/generated.ts b/lunatrace/bsl/frontend/src/api/generated.ts index 8965da27d..7c9137727 100644 --- a/lunatrace/bsl/frontend/src/api/generated.ts +++ b/lunatrace/bsl/frontend/src/api/generated.ts @@ -171,6 +171,12 @@ export type BuildData_VulnerableRelease = { trivially_updatable: Scalars['String']; }; +export type CreatePullRequestForVulnerabilityResponse = { + __typename?: 'CreatePullRequestForVulnerabilityResponse'; + pullRequestUrl: Scalars['String']; + success?: Maybe; +}; + /** Boolean expression to compare columns of type "Float". All fields are combined with logical 'AND'. */ export type Float_Comparison_Exp = { _eq?: InputMaybe; @@ -3760,6 +3766,7 @@ export enum Manifests_Update_Column { /** mutation root */ export type Mutation_Root = { __typename?: 'mutation_root'; + createPullRequestForVulnerability?: Maybe; /** delete data from the table: "builds" */ delete_builds?: Maybe; /** delete single row from the table: "builds" */ @@ -3854,6 +3861,16 @@ export type Mutation_Root = { }; +/** mutation root */ +export type Mutation_RootCreatePullRequestForVulnerabilityArgs = { + new_package_slug: Scalars['String']; + old_package_slug: Scalars['String']; + package_manifest_path: Scalars['String']; + project_id: Scalars['uuid']; + vulnerability_id: Scalars['uuid']; +}; + + /** mutation root */ export type Mutation_RootDelete_BuildsArgs = { where: Builds_Bool_Exp; @@ -6330,7 +6347,7 @@ export type Query_RootVulnerability_Cisa_Known_ExploitedArgs = { export type Query_RootVulnerability_Cisa_Known_Exploited_By_PkArgs = { - id: Scalars['uuid']; + cve: Scalars['String']; }; @@ -7681,7 +7698,7 @@ export type Subscription_RootVulnerability_Cisa_Known_ExploitedArgs = { export type Subscription_RootVulnerability_Cisa_Known_Exploited_By_PkArgs = { - id: Scalars['uuid']; + cve: Scalars['String']; }; @@ -8429,7 +8446,7 @@ export type Vulnerability_Bool_Exp = { /** columns and relationships of "vulnerability.cisa_known_exploited" */ export type Vulnerability_Cisa_Known_Exploited = { __typename?: 'vulnerability_cisa_known_exploited'; - cve?: Maybe; + cve: Scalars['String']; date_added: Scalars['date']; due_date: Scalars['date']; id: Scalars['uuid']; @@ -9349,6 +9366,17 @@ export type InsertProjectAccessTokenMutationVariables = Exact<{ export type InsertProjectAccessTokenMutation = { __typename?: 'mutation_root', insert_project_access_tokens_one?: { __typename?: 'project_access_tokens', id: any } | null }; +export type CreateGitHubPullRequestForVulnMutationVariables = Exact<{ + project_id: Scalars['uuid']; + vulnerability_id: Scalars['uuid']; + old_package_slug: Scalars['String']; + new_package_slug: Scalars['String']; + package_manifest_path: Scalars['String']; +}>; + + +export type CreateGitHubPullRequestForVulnMutation = { __typename?: 'mutation_root', createPullRequestForVulnerability?: { __typename?: 'CreatePullRequestForVulnerabilityResponse', success?: boolean | null, pullRequestUrl: string } | null }; + export type InsertIgnoredVulnerabilitiesMutationVariables = Exact<{ objects: Array | Ignored_Vulnerabilities_Insert_Input; }>; @@ -10308,6 +10336,20 @@ export const InsertProjectAccessTokenDocument = ` } } `; +export const CreateGitHubPullRequestForVulnDocument = ` + mutation CreateGitHubPullRequestForVuln($project_id: uuid!, $vulnerability_id: uuid!, $old_package_slug: String!, $new_package_slug: String!, $package_manifest_path: String!) { + createPullRequestForVulnerability( + project_id: $project_id + vulnerability_id: $vulnerability_id + old_package_slug: $old_package_slug + new_package_slug: $new_package_slug + package_manifest_path: $package_manifest_path + ) { + success + pullRequestUrl + } +} + `; export const InsertIgnoredVulnerabilitiesDocument = ` mutation InsertIgnoredVulnerabilities($objects: [ignored_vulnerabilities_insert_input!]!) { insert_ignored_vulnerabilities( @@ -10500,6 +10542,9 @@ const injectedRtkApi = api.injectEndpoints({ InsertProjectAccessToken: build.mutation({ query: (variables) => ({ document: InsertProjectAccessTokenDocument, variables }) }), + CreateGitHubPullRequestForVuln: build.mutation({ + query: (variables) => ({ document: CreateGitHubPullRequestForVulnDocument, variables }) + }), InsertIgnoredVulnerabilities: build.mutation({ query: (variables) => ({ document: InsertIgnoredVulnerabilitiesDocument, variables }) }), diff --git a/lunatrace/bsl/frontend/src/api/graphql/mutateCreateGitHubPullRequestForVuln.graphql b/lunatrace/bsl/frontend/src/api/graphql/mutateCreateGitHubPullRequestForVuln.graphql new file mode 100644 index 000000000..1904ec2ee --- /dev/null +++ b/lunatrace/bsl/frontend/src/api/graphql/mutateCreateGitHubPullRequestForVuln.graphql @@ -0,0 +1,18 @@ +mutation CreateGitHubPullRequestForVuln( + $project_id: uuid!, + $vulnerability_id: uuid!, + $old_package_slug: String!, + $new_package_slug: String!, + $package_manifest_path: String! +) { + createPullRequestForVulnerability( + project_id: $project_id, + vulnerability_id: $vulnerability_id, + old_package_slug: $old_package_slug, + new_package_slug: $new_package_slug, + package_manifest_path: $package_manifest_path + ) { + success + pullRequestUrl + } +} diff --git a/lunatrace/bsl/frontend/src/pages/project/builds/details/VulnerablePackageListWrapper.tsx b/lunatrace/bsl/frontend/src/pages/project/builds/details/VulnerablePackageListWrapper.tsx index 769f995ee..3dd0a7982 100644 --- a/lunatrace/bsl/frontend/src/pages/project/builds/details/VulnerablePackageListWrapper.tsx +++ b/lunatrace/bsl/frontend/src/pages/project/builds/details/VulnerablePackageListWrapper.tsx @@ -44,10 +44,10 @@ export const VulnerablePackageListWrapper: React.FC { - // severity state for modern tree data, legacy has its own state and doesnt use this + // severity state for modern tree data, legacy has its own state and doesn't use this const [severity, setSeverity] = useState('Critical'); - // data for modern tree, legacy doesnt use this + // data for modern tree, legacy doesn't use this const { data: vulnerableReleasesData, isLoading, diff --git a/lunatrace/bsl/frontend/src/pages/project/builds/details/vulnerable-packages/vulnerable-package-card/PackageUpdatablePopOver.tsx b/lunatrace/bsl/frontend/src/pages/project/builds/details/vulnerable-packages/vulnerable-package-card/PackageUpdatablePopOver.tsx index e680b063e..f5675404d 100644 --- a/lunatrace/bsl/frontend/src/pages/project/builds/details/vulnerable-packages/vulnerable-package-card/PackageUpdatablePopOver.tsx +++ b/lunatrace/bsl/frontend/src/pages/project/builds/details/vulnerable-packages/vulnerable-package-card/PackageUpdatablePopOver.tsx @@ -12,14 +12,22 @@ * */ import React from 'react'; -import { NavLink, OverlayTrigger, Popover, Tooltip } from 'react-bootstrap'; +import { Button, NavLink, OverlayTrigger, Popover, Tooltip } from 'react-bootstrap'; import { CopyBlock, tomorrowNight } from 'react-code-blocks'; +import { BsGithub } from 'react-icons/bs'; import { FcUpload } from 'react-icons/fc'; import useBreakpoint from '../../../../../../hooks/useBreakpoint'; import { isDirectDep } from '../../../../../../utils/package'; import { VulnerablePackage } from '../types'; -export const PackageUpdatablePopOver: React.FC<{ pkg: VulnerablePackage }> = ({ pkg }) => { + +export const PackageUpdatablePopOver: React.FC<{ + pkg: VulnerablePackage; + onClickUpdate: (pkg: VulnerablePackage) => Promise; +}> = ({ pkg, onClickUpdate }) => { + const [creatingPr, setCreatingPr] = React.useState(false); + const [showPopOver, setShowPopOver] = React.useState(false); + const trivialUpdateStatus = pkg.trivially_updatable; if (!trivialUpdateStatus || trivialUpdateStatus === 'no') { @@ -30,13 +38,32 @@ export const PackageUpdatablePopOver: React.FC<{ pkg: VulnerablePackage }> = ({ return ( Trivially Updatable - - A fix is available within the semver range this package was requested with, meaning that the{' '} - project lockfile is probably the only thing constraining the package to the vulnerable - version. -
+ +
+ +
+ Clicking this button will open a Pull Request to update the project's lockfile. +
+ <> + A fix is available within the semver range this package was requested with, meaning that the{' '} + project lockfile is likely constraining the package to the vulnerable version. + +
{isDirectDep(pkg) ? ( - <> +
This command will update the package: = ({ theme={tomorrowNight} codeBlock /> - +
) : ( - <> +
This package is a transitive (deep) dependency. Due to constraints of NPM and Yarn, - updating it is only possible by manually deleting it from your project's lockfile (or deleting your - entire lockfile) and then re-running your package install command (npm install) to fetch the latest - version. Automated lockfile patching is currently in development by LunaTrace. - + updating it is only possible using a special tool like{' '} + + @lunatrace/npm-package-cli + {' '} + (which powers the automated patcher above). +
)}
@@ -73,7 +106,14 @@ export const PackageUpdatablePopOver: React.FC<{ pkg: VulnerablePackage }> = ({ return ( <> {' '} - + setShowPopOver(nextShow)} + > {trivialUpdateStatus === 'partially' ? 'partially ' : ''}updatable diff --git a/lunatrace/bsl/frontend/src/pages/project/builds/details/vulnerable-packages/vulnerable-package-card/VulnerablePackageCardHeader.tsx b/lunatrace/bsl/frontend/src/pages/project/builds/details/vulnerable-packages/vulnerable-package-card/VulnerablePackageCardHeader.tsx index 27710faa2..dd30abf4c 100644 --- a/lunatrace/bsl/frontend/src/pages/project/builds/details/vulnerable-packages/vulnerable-package-card/VulnerablePackageCardHeader.tsx +++ b/lunatrace/bsl/frontend/src/pages/project/builds/details/vulnerable-packages/vulnerable-package-card/VulnerablePackageCardHeader.tsx @@ -26,11 +26,13 @@ import { PackageUpdatablePopOver } from './PackageUpdatablePopOver'; interface VulnerablePackageCardHeaderProps { ignored: boolean; pkg: VulnerablePackage; + onClickUpdate: (pkg: VulnerablePackage) => Promise; } export const VulnerablePackageCardHeader: React.FunctionComponent = ({ pkg, ignored, + onClickUpdate, }) => { const recommendedVersion = semver.rsort([...pkg.fix_versions])[0]; return ( @@ -50,7 +52,7 @@ export const VulnerablePackageCardHeader: React.FunctionComponent )} - + { const [showConfirmation, setShowConfirmation] = useState(false); const [insertVulnIgnore, insertVulnIgnoreState] = api.useInsertIgnoredVulnerabilitiesMutation(); + const [createGitHubPullRequestForVuln] = api.useCreateGitHubPullRequestForVulnMutation(); + const [ignoreNote, setIgnoreNote] = useState(''); const { project_id } = useParams(); const [shouldFilterFindingsBySeverity, setShouldFilterFindingsBySeverity] = useState(true); + const [patchPullRequestUrl, setPatchPullRequestUrl] = useState(undefined); + const findingsAboveSeverity = pkg.affected_by.filter((affectedByVuln) => { return !affectedByVuln.beneath_minimum_severity || !shouldFilterFindingsBySeverity; }); @@ -65,6 +70,8 @@ export const VulnerablePackageMain: React.FunctionComponent f.ignored); + const recommendedVersion = semver.rsort([...pkg.fix_versions])[0]; + const bulkIgnoreVulns = async () => { if (!project_id) { throw new Error('attempted to ignore a vuln but no project id is in the url'); @@ -82,7 +89,7 @@ export const VulnerablePackageMain: React.FunctionComponent) => void,children:React.ReactNode } + { onClick: (e: React.MouseEvent) => void; children: React.ReactNode } >(({ children, onClick }, ref) => ( {renderIgnoreUi()} - + + {patchPullRequestUrl && ( + + )}