Skip to content

Commit

Permalink
built beginnings of dependency tree viewer
Browse files Browse the repository at this point in the history
Signed-off-by: Kaden Emley <kemley@mitre.org>
  • Loading branch information
kemley76 committed Aug 9, 2024
1 parent 3c2517b commit f5e469c
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 58 deletions.
60 changes: 58 additions & 2 deletions apps/frontend/src/components/cards/sbomview/ComponentContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,46 @@

<!-- Viewing Nested Structures -->
<v-treeview v-if="tab.treeData" open-all :items="tab.treeData" />

<!-- Viewing a list of this component's direct dependencies -->
<v-simple-table v-if="tab.relatedComponents" dense>
<template #default>
<thead>
<tr>
<td>Name</td>
<td>Description</td>
<td />
<td />
</tr>
</thead>
<tbody>
<tr v-for="(component, i) in tab.relatedComponents" :key="i">
<td>{{ component.name }}</td>
<td>{{ component.description }}</td>
<td>
<v-chip
small
outlined
@click="
$emit('show-component-in-table', component['bom-ref'])
"
>Go to Component Table</v-chip
>
</td>
<td>
<v-chip
small
outlined
@click="
$emit('show-component-in-tree', component['bom-ref'])
"
>Go to Dependency Tree</v-chip
>
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-tab-item>
</v-tabs-items>
</div>
Expand All @@ -67,6 +107,7 @@ interface Tab {
name: string;
tableData?: TableData;
treeData?: Treeview[];
relatedComponents?: SBOMComponent[];
}
type TableData = {columns: string[]; rows: string[][]};
Expand Down Expand Up @@ -170,7 +211,7 @@ function generateTabs(
};
for (const [key, value] of Object.entries(object)) {
if (customTabs.includes(key)) continue;
if (customTabs.includes(key) || key.startsWith('_')) continue;
if (value instanceof Object) {
tabs.push({
name: `${prefix}${_.startCase(key)}`,
Expand Down Expand Up @@ -232,6 +273,9 @@ export default class ComponentContent extends Vue {
@Prop({type: Array, required: false, default: () => []})
readonly vulnerabilities!: ContextualizedControl[];
@Prop({type: Array, required: false}) readonly dependencies?: SBOMComponent[];
@Prop({type: Array, required: false}) readonly parents?: SBOMComponent[];
// stores the state of the tab selected
tabs = {tab: null};
Expand All @@ -245,7 +289,6 @@ export default class ComponentContent extends Vue {
*/
get computedTabs(): Tab[] {
const tabs: Tab[] = [];
if (this.metadata) {
// for displaying top level component data
tabs.push(...generateTabs(this.metadata, 'Metadata - '));
Expand All @@ -270,6 +313,19 @@ export default class ComponentContent extends Vue {
});
}
if (this.dependencies?.length) {
tabs.push({
name: 'Dependencies',
relatedComponents: this.dependencies
});
}
if (this.parents?.length) {
tabs.push({
name: 'Parents',
relatedComponents: this.parents
});
}
return tabs;
}
}
Expand Down
51 changes: 45 additions & 6 deletions apps/frontend/src/components/cards/sbomview/ComponentTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
:expanded.sync="expanded"
show-expand
:items-per-page="-1"
item-key="key"
item-key="_key"
hide-default-footer
>
<!-- fixed-header
height="calc(100vh - 250px)" -->
<template #top>
<v-card-title>
Component View Data
Components
<v-spacer />

<!-- Table settings menu -->
Expand Down Expand Up @@ -101,10 +101,14 @@
</template>

<template #expanded-item="{headers, item}">
<td :colspan="headers.length">
<td v-if="expanded.includes(item)" :colspan="headers.length">
<ComponentContent
:component="item"
:vulnerabilities="affectingVulns.get(item['bom-ref'])"
:dependencies="componentDependencies(item)"
:parents="componentParents(item)"
@show-component-in-table="showComponentInTable"
@show-component-in-tree="showComponentInTree"
/>
</td>
</template>
Expand All @@ -121,17 +125,22 @@ import {ContextualizedControl, severities, Severity} from 'inspecjs';
import _ from 'lodash';
import Vue from 'vue';
import Component from 'vue-class-component';
import {Prop, Ref} from 'vue-property-decorator';
import {Prop} from 'vue-property-decorator';
import ComponentContent from './ComponentContent.vue';
import {getVulnsFromBomRef, SBOMComponent} from '@/utilities/sbom_util';
import {
getVulnsFromBomRef,
SBOMComponent,
getStructuredSbomDependencies,
sbomDependencyToComponents,
SBOMDependency
} from '@/utilities/sbom_util';
@Component({
components: {
ComponentContent
}
})
export default class ComponentTable extends Vue {
@Ref('controlTableTitle') readonly controlTableTitle!: Element;
@Prop({type: String, required: false}) readonly searchTerm!: string;
componentRef = this.$route.query.componentRef ?? null;
Expand Down Expand Up @@ -224,6 +233,28 @@ export default class ComponentTable extends Vue {
return vulnMap;
}
get structuredDependencies(): Map<string, SBOMDependency> {
return getStructuredSbomDependencies();
}
componentDependencies(component: SBOMComponent): SBOMComponent[] | undefined {
if (!component['bom-ref']) return;
const dependency = this.structuredDependencies.get(component['bom-ref']);
if (!dependency) return;
return sbomDependencyToComponents(dependency, this.components);
}
componentParents(component: SBOMComponent): SBOMComponent[] | undefined {
const ref = component['bom-ref'];
if (!ref) return;
return this.components.filter((c) => {
if (!c['bom-ref']) return false;
const dependency = this.structuredDependencies.get(c['bom-ref']);
return dependency?.dependsOn?.includes(ref);
});
}
severityColor(severity: string): string {
return `severity${_.startCase(severity)}`;
}
Expand All @@ -236,6 +267,14 @@ export default class ComponentTable extends Vue {
// returns the list of severities defined by inspecJS
return [...severities];
}
showComponentInTable(ref: string) {
this.$emit('show-component-in-table', ref);
}
showComponentInTree(ref: string) {
this.$emit('show-component-in-tree', ref);
}
}
</script>

Expand Down
116 changes: 116 additions & 0 deletions apps/frontend/src/components/cards/sbomview/DependencyTree.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<div>
<v-treeview
:items="loadedDependencies"
:load-children="getStructure"
dense
activatable
>
<!-- Searching can be added, but it might be more complex if we are loading
the structure only when that node is opened. So it cannot find deeply nested components
that match the search term automatically -->

<!-- TODO: change the ref for a unique identifier like component.key -->
<template #append="{item, active}">
<v-chip
v-if="active"
@click="$emit('show-component-in-table', item.ref)"
>
Open in Component Table
</v-chip>
</template>
</v-treeview>
</div>
</template>

<script lang="ts">
import {FilteredDataModule} from '@/store/data_filters';
import {
getSbomMetadata,
SBOMDependency,
getStructuredSbomDependencies
} from '@/utilities/sbom_util';
import _ from 'lodash';
import Vue from 'vue';
import Component from 'vue-class-component';
import {Prop} from 'vue-property-decorator';
interface TreeNode {
name: string;
children?: DependencyStructure[];
id: number;
}
type DependencyStructure = TreeNode & SBOMDependency;
@Component({
components: {}
})
export default class DependencyTree extends Vue {
@Prop({type: String, required: false}) readonly searchTerm!: string;
@Prop({type: String, required: false}) readonly targetComponent!:
| string
| null;
loadedDependencies: DependencyStructure[] = [];
nextId = 0;
mounted() {
this.loadedDependencies = this.rootComponentRefs.map(this.getRootStructure);
console.log('Mounted ' + this.targetComponent);
}
severityColor(severity: string): string {
return `severity${_.startCase(severity)}`;
}
get structuredDependencies() {
return getStructuredSbomDependencies();
}
get rootComponentRefs(): string[] {
return FilteredDataModule.sboms(FilteredDataModule.selected_sbom_ids)
.map(getSbomMetadata)
.map((result) =>
result.ok ? _.get(result.value, 'component.bom-ref', '') : ''
);
}
getRootStructure(ref: string): DependencyStructure {
const root = this.structuredDependencies.get(ref);
const id = this.nextId;
this.nextId += 1;
if (root) return {...root, name: ref, children: [], id};
return {ref, name: ref, id, children: [], dependsOn: []};
}
getStructure(item: DependencyStructure) {
const children: DependencyStructure[] = [];
for (const ref of item.dependsOn || []) {
const child = this.structuredDependencies.get(ref);
if (child) {
if (child.dependsOn?.length)
// used to indicate that this tree node has more dependents to load in
children.push({
...child,
name: child.ref,
children: [],
id: this.nextId
});
else children.push({...child, name: child.ref, id: this.nextId});
this.nextId += 1;
}
}
item.children = children;
}
}
/**
* TODO: Figure out why loading multiple breaks everything
* comment code thoroughly
* add buttons to reference back to component table (both ways)
* fix issue with things closing
* add bread crumbs thing
*/
</script>

<style scoped></style>
4 changes: 2 additions & 2 deletions apps/frontend/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {EvaluationModule} from '@/store/evaluations';
import {ServerModule} from '@/store/server';
import Admin from '@/views/Admin.vue';
import Compare from '@/views/Compare.vue';
import SbomView from '@/views/SbomView.vue';
import Sbom from '@/views/Sbom.vue';
import Groups from '@/views/Groups.vue';
import Landing from '@/views/Landing.vue';
import Login from '@/views/Login.vue';
Expand Down Expand Up @@ -48,7 +48,7 @@ const router = new Router({
{
path: '/sbom-view',
name: 'sbom',
component: SbomView,
component: Sbom,
meta: {requiresAuth: true, hasIdParams: true}
},
{
Expand Down
30 changes: 3 additions & 27 deletions apps/frontend/src/store/data_filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ import {execution_unique_key} from '@/utilities/format_util';
import {
componentFitsSeverityFilter,
getSbomComponents,
getVulnsFromBomRef,
isOnlySbomFileId,
isSbomFileId,
SBOMComponent
} from '@/utilities/sbom_util';
import {Result} from '@mitre/hdf-converters/src/utils/result';
import {
ContextualizedControl,
ContextualizedProfile,
Expand Down Expand Up @@ -521,39 +519,17 @@ export class FilteredData extends VuexModule {
const evaluations = this.sboms(filter.fromFile);
const components: SBOMComponent[] = [];
const controls = this.controls(filter);
// grab every component from each section and apply a filter if necessary
/* return evaluations
.map(getSbomComponents)
.filter((result) => result.ok)
.map((result: {value: SBOMComponentsResult}) => result.value) // compiler typechecking doesn't know that `value` is guaranteed
.flatMap((pair) => {
const executionKey = execution_unique_key(pair.evaluation);
return pair.components.filter((component) => {
if (!component.affectingVulnerabilities?.length)
return filter.severity?.includes('none');
const vulns = [];
// collect all the controls corresponding to the ids in `affectingVulnerabilities`
for (const vulnRef of component.affectingVulnerabilities) {
const vuln = getVulnsFromBomRef(vulnRef, controls);
if (vuln.ok) vulns.push(vuln!.value);
}
// add the component if it has a vulnerability with an allowable severity
return vulns.some((v) => filter.severity?.includes(v.hdf.severity));
}).map(component => ({...component, key: `${executionKey}-${component['bom-ref']}`}))
}); */

// grab every component from each section and apply a filter if necessary
for (const evaluation of evaluations) {
const sbomComponents: Result<SBOMComponent[], null> =
getSbomComponents(evaluation);
if (!sbomComponents.ok) continue;
for (const component of sbomComponents.value) {
for (const component of getSbomComponents(evaluation)) {
const key = `${execution_unique_key(evaluation)}-${component['bom-ref']}`;
// filter components by their affecting vulnerabilities
if (
!filter.severity ||
componentFitsSeverityFilter(component, filter.severity, controls)
)
components.push({...component, key});
components.push({...component, _key: key});
}
}
return components;
Expand Down
Loading

0 comments on commit f5e469c

Please sign in to comment.