Skip to content

Commit 0dd9501

Browse files
Merge pull request #772 from lunasec-io/no-tech-debt-in-vuln-schema
Better vulnerability data Former-commit-id: 35eb8d7 Former-commit-id: 0a981cf3ffbd8d3494e4e0c515d4a225f5d7ee5e
2 parents b6df684 + 97e8d6f commit 0dd9501

File tree

30 files changed

+1928
-344
lines changed

30 files changed

+1928
-344
lines changed

lunatrace/bsl/frontend/src/api/graphql/getBuildDetails.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ query GetBuildDetails($build_id: uuid!, $project_id: uuid!) {
2121
last_successful_fetch
2222
package_manager
2323
vulnerabilities {
24+
id
2425
affected_range_events {
2526
event
2627
type

lunatrace/bsl/frontend/src/dependency-tree/builds-dependency-tree.ts

Lines changed: 109 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,31 @@ import { RawDependencyRelationship } from './types';
4747
export interface BuildDependencyPartial {
4848
id: string;
4949
dependend_by_relationship_id: string;
50-
// root_range: string | null;
51-
// sub_dependency_relationships: Array<{
52-
// range: string;
53-
// to_dependency: string;
54-
// }>;
50+
range: string;
51+
release_id: string;
52+
release: {
53+
version: string;
54+
};
55+
package: {
56+
vulnerabilities: Array<Vulnerability>;
57+
};
58+
}
59+
60+
interface Vulnerability {
61+
id: string;
62+
ranges: Array<{
63+
introduced: string;
64+
fixed: string;
65+
}>;
66+
triviallyUpdatable?: boolean; // We add this by determining something can be updated to a non-vulnerable version without violating semver
5567
}
5668

5769
// Recursive type to model the recursive tree we are building. Simply adds the field "dependents" which points down to more nodes.
58-
type BuildDependencyTreeNode<D> = D & {
59-
dependents: Array<BuildDependencyTreeNode<D>>;
70+
type TreeNode<D> = D & {
71+
dependents: Array<TreeNode<D>>;
6072
};
6173

62-
export type DependencyChain<D> = Array<BuildDependencyTreeNode<D>>;
74+
// export type DependencyChain<D> = Array<TreeNode<D>>;
6375

6476
/**
6577
* hasura and graphql doesn't allow us to fetch recursive stuff, so we build the tree ourselves on the client
@@ -69,20 +81,43 @@ export type DependencyChain<D> = Array<BuildDependencyTreeNode<D>>;
6981
* @public flatDeps The original set of dependencies that was passed in. Preserves types.
7082
*/
7183
export class DependencyTree<BuildDependency extends BuildDependencyPartial> {
72-
public tree: Array<BuildDependencyTreeNode<BuildDependency>>;
84+
public readonly tree: Array<TreeNode<BuildDependency>>;
85+
public readonly flatVulns: Array<Vulnerability>;
86+
constructor(public readonly flatDeps: Array<BuildDependency>) {
87+
// Go and clean out all the vulnerabilities that don't apply to this version since the DB doesn't know how to do that yet
88+
this.flatVulns = [];
89+
flatDeps.forEach((dep) => {
90+
dep.package.vulnerabilities = dep.package.vulnerabilities.filter((vuln) => {
91+
const vulnerableRange = this.convertRangesToSemverRange(vuln.ranges);
92+
const isVulnerable = semver.satisfies(dep.release.version, vulnerableRange);
93+
return isVulnerable;
94+
});
95+
// Mark the vulns that can be trivially updated
96+
dep.package.vulnerabilities.forEach((vuln) => {
97+
vuln.triviallyUpdatable = this.checkVulnTriviallyUpdatable(dep.range, vuln);
98+
// Also add it ot the flat vuln list for easy access
99+
this.flatVulns.push(vuln);
100+
});
101+
});
73102

74-
constructor(public flatDeps: BuildDependency[]) {
75103
// define an internal recursive function that builds each node
76104
// it's in the constructor because this has access to the class generic and the flatDeps
77105
// recursive stuff is always a little hairy but this is really quite dead simple
78-
function recursivelyBuildNode(dep: BuildDependency): BuildDependencyTreeNode<BuildDependency> {
106+
const cycleCheckIds: Array<string> = [];
107+
function recursivelyBuildNode(dep: BuildDependency): TreeNode<BuildDependency> {
108+
// Check for cycles, just in case
109+
if (cycleCheckIds.includes(dep.id)) {
110+
throw new Error('Dependency cycle detected!');
111+
}
112+
cycleCheckIds.push(dep.id);
79113
// Find every dep that points back at this dep
80114
const unbuiltDependents = flatDeps.filter(
81115
(potentialDependent) => potentialDependent.dependend_by_relationship_id === dep.id
82116
);
83-
// For each dependent, populate its own dependents recursively
117+
// For each dependent, add it to our list of dependents and populate its own dependents recursively
84118
const dependents = unbuiltDependents.map(recursivelyBuildNode);
85-
return { ...dep, dependents };
119+
const builtNode = Object.freeze({ ...dep, dependents });
120+
return builtNode;
86121
}
87122
// start with the root dependencies
88123
const rootDeps = flatDeps.filter(function filterRoots(d) {
@@ -92,8 +127,68 @@ export class DependencyTree<BuildDependency extends BuildDependencyPartial> {
92127
this.tree = rootDeps.map((rootDep) => recursivelyBuildNode(rootDep));
93128
}
94129

130+
private checkVulnTriviallyUpdatable(requestedRange: string, vuln: Vulnerability): boolean {
131+
const fixedVersions: string[] = [];
132+
vuln.ranges.forEach((range) => {
133+
if (range.fixed) {
134+
fixedVersions.push(range.fixed);
135+
}
136+
});
137+
return fixedVersions.some((fixVersion) => {
138+
return semver.satisfies(requestedRange, fixVersion);
139+
});
140+
}
141+
142+
// only useful if you need the nodes in tree form. Otherwise, use flatDeps
143+
// todo: not currently used, delete if unused
144+
public collectAllTreeNodes(): TreeNode<BuildDependency>[] {
145+
const allDepNodes: TreeNode<BuildDependency>[] = [];
146+
function recurseNode(dep: TreeNode<BuildDependency>) {
147+
allDepNodes.push(dep);
148+
dep.dependents.forEach(recurseNode);
149+
}
150+
this.tree.forEach(recurseNode);
151+
return allDepNodes;
152+
}
153+
154+
public convertRangesToSemverRange(ranges: Array<{ introduced: string; fixed: string }>): semver.Range {
155+
const vulnerableRanges: string[] = [];
156+
ranges.forEach((range) => {
157+
if (range.introduced && range.fixed) {
158+
vulnerableRanges.push(`>=${range.introduced} <${range.fixed}`);
159+
} else if (range.introduced) {
160+
vulnerableRanges.push(`>=${range.introduced}`);
161+
}
162+
});
163+
const vulnerableRangesString = vulnerableRanges.join(' || '); // Just put them all in one big range and let semver figure it out
164+
const semverRange = new semver.Range(vulnerableRangesString);
165+
return semverRange;
166+
}
167+
168+
// This will no longer be needed once the UI is only using this tree to show vulnerabilities
169+
// for now, since grype is still in the loop, we need to cross reference information out of this tree using vuln ids
170+
public checkIfVulnInstancesTriviallyUpdatable(vulnId: string): 'all' | 'partial' | 'none' | 'not-found' {
171+
const vulns = this.flatVulns.filter((v) => v.id === vulnId);
172+
if (vulns.length === 0) {
173+
console.warn(
174+
`failed to find a vuln with id ${vulnId} in the tree.
175+
It may be that the tree determined this vulnerability did not apply and it was removed.
176+
Grype must have thought differently`
177+
);
178+
return 'not-found';
179+
}
180+
const vulnsUpdatable = vulns.filter((vuln) => vuln.triviallyUpdatable);
181+
if (vulnsUpdatable.length === vulns.length) {
182+
return 'all';
183+
}
184+
if (vulnsUpdatable.length === 0) {
185+
return 'none';
186+
}
187+
return 'partial';
188+
}
189+
95190
// Can show us why a dependency is installed
96-
// TODO: commented o until we need it and schema is solidified
191+
// TODO: commented out until we need it and schema is solidified
97192
// public showDependencyChainsOfPackage(
98193
// depId: string,
99194
// prependToExistingChain: string[] = []
@@ -118,71 +213,4 @@ export class DependencyTree<BuildDependency extends BuildDependencyPartial> {
118213
// });
119214
// return chains;
120215
// }
121-
122-
/**
123-
* Finds a dependency out of the list of deps using its id
124-
* @param depId
125-
* @private
126-
* @throws Error
127-
*/
128-
private lookupDepById(depId: string) {
129-
const dep = this.flatDeps.find((d) => d.id === depId);
130-
if (!dep) {
131-
throw new Error(
132-
`Couldnt find dependency during lookup, make sure DependencyTree was constructed with this dep included: ${depId}`
133-
);
134-
}
135-
return dep;
136-
}
137-
138-
/**
139-
* Gets all the ranges that a package was requested with
140-
* Note that this wont show ranges of different versions of the package in the tree, just ranges that resolved to the exact version/release.
141-
* ex: if you had react 4.1.1 and react 5.1.1 in your tree, this would output something like `['>4.0.0, '4.1.1'] and not '>5.0.0' because that's resolved as a different package in the package-lock
142-
* @param depId
143-
* @throws Error
144-
*/
145-
146-
// public getRangesRequestedOfPackage(depId: string): string[] {
147-
// const ranges: string[] = [];
148-
//
149-
// // find the dep and see if it was required directly by the project
150-
// const dep = this.lookupDepById(depId);
151-
// if (dep.root_range) {
152-
// ranges.push(dep.root_range);
153-
// }
154-
//
155-
// // go through all the deps and look for transitive dependencies on this dep
156-
// this.flatDeps.forEach((d) => {
157-
// // pick out what relationships in the tree point to this package
158-
// const relationship = d.sub_dependency_relationships.find((r) => r.to_dependency === depId);
159-
// if (relationship) {
160-
// ranges.push(relationship.range);
161-
// }
162-
// });
163-
// return ranges;
164-
// }
165-
166-
/**
167-
* See if we can update a package to a fixed version(s) without violating semver
168-
* @param toVersions Since there might be multiple fix versions for a vulnerability, take a list of possible ones we could update to in `toVersions`
169-
* @param depId
170-
*/
171-
// public determinePackageTriviallyUpdatable(toVersions: string[], depId: string): boolean {
172-
// return toVersions.some((toVersion) => {
173-
// // I think coercing like this is a good idea because it will make things like `1.2.3-hotfix` appear valid against the ranges, and a lot of patches might be like that
174-
// // Maybe it will do that automatically, need to test. Awful docs for this library.
175-
// const coercedVersion = semver.coerce(toVersion);
176-
// if (!semver.valid(coercedVersion) || !coercedVersion) {
177-
// throw new Error(
178-
// 'Invalid version specified when checking if updatable, probably bad OSV data from the vulnerability data source fixes field'
179-
// );
180-
// }
181-
//
182-
// const ranges = this.getRangesRequestedOfPackage(depId);
183-
// return ranges.every((range) => {
184-
// semver.satisfies(coercedVersion, range);
185-
// });
186-
// });
187-
// }
188216
}

lunatrace/bsl/frontend/src/pages/project/builds/VulnerablePackageListItem/VulnerabilityTableItem.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const VulnerabilityTableItem: React.FC<VulnerabilityTableItemProps> = ({
5555
const ignoredNotePretty = rawNote
5656
? `Ignored with note: ${rawNote}`
5757
: 'Vulnerability has been ignored without a reason.';
58+
return <span>{ignoredNotePretty}</span>;
5859
};
5960
const ignoreVuln = async () => {
6061
await insertVulnIgnore({
@@ -71,6 +72,9 @@ export const VulnerabilityTableItem: React.FC<VulnerabilityTableItemProps> = ({
7172
};
7273

7374
const getIgnoreColumn = () => {
75+
if (findingIsIgnored) {
76+
return null;
77+
}
7478
if (insertVulnIgnoreState.isLoading) {
7579
return <Spinner size="sm" animation="border" />;
7680
}

lunatrace/bsl/frontend/src/pages/project/dashboard/Main.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,8 @@ export const ProjectDashboardMain: React.FunctionComponent<ProjectDashboardMainP
5757
</Accordion.Body>
5858
</Accordion.Item>
5959
</Accordion>
60-
<hr />
61-
<ManifestDrop project_id={project.id} />
6260
<ConditionallyRender if={isAdmin}>
61+
<hr />
6362
<ProjectCloneForAdmin project={project} />
6463
</ConditionallyRender>
6564
</>

lunatrace/bsl/frontend/src/pages/project/dashboard/ManifestDrop.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ export const ManifestDrop: React.FunctionComponent<{ project_id: string; forHome
153153
return (
154154
<span>
155155
<FilePlus className="me-1 mb-1" />
156-
Click here or drap-and-drop a manifest file or compressed project to manually submit a build.
156+
Click here or drap-and-drop a manifest file to manually submit a build.
157157
<br />
158-
(ex: package-lock.json, my-project.jar, my-project.zip)
158+
(ex: package-lock.json)
159159
</span>
160160
);
161161
};

lunatrace/bsl/hasura/metadata/databases/lunatrace/tables/public_build_dependency_relationship.yaml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,35 @@ insert_permissions:
1616
permission:
1717
check: {}
1818
columns:
19-
- labels
20-
- range
2119
- build_id
2220
- depended_by_relationship_id
2321
- id
22+
- labels
23+
- project_path
24+
- range
2425
- release_id
2526
backend_only: false
2627
select_permissions:
2728
- role: service
2829
permission:
2930
columns:
30-
- labels
31-
- range
3231
- build_id
3332
- depended_by_relationship_id
3433
- id
34+
- labels
35+
- project_path
36+
- range
3537
- release_id
3638
filter: {}
3739
- role: user
3840
permission:
3941
columns:
40-
- labels
41-
- range
4242
- build_id
4343
- depended_by_relationship_id
4444
- id
45+
- labels
46+
- project_path
47+
- range
4548
- release_id
4649
filter:
4750
build:
@@ -54,11 +57,12 @@ update_permissions:
5457
- role: service
5558
permission:
5659
columns:
57-
- labels
58-
- range
5960
- build_id
6061
- depended_by_relationship_id
6162
- id
63+
- labels
64+
- project_path
65+
- range
6266
- release_id
6367
filter: {}
6468
check: {}

lunatrace/bsl/hasura/metadata/databases/lunatrace/tables/tables.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
- "!include vulnerability_affected_version.yaml"
3939
- "!include vulnerability_credit.yaml"
4040
- "!include vulnerability_equivalent.yaml"
41+
- "!include vulnerability_range.yaml"
4142
- "!include vulnerability_reference.yaml"
4243
- "!include vulnerability_severity.yaml"
4344
- "!include vulnerability_vulnerability.yaml"

lunatrace/bsl/hasura/metadata/databases/lunatrace/tables/vulnerability_affected.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ array_relationships:
2323
table:
2424
schema: vulnerability
2525
name: affected_version
26+
- name: ranges
27+
using:
28+
foreign_key_constraint_on:
29+
column: affected_id
30+
table:
31+
schema: vulnerability
32+
name: range
2633
insert_permissions:
2734
- role: service
2835
permission:

0 commit comments

Comments
 (0)