Skip to content

Commit

Permalink
overhaul of sbom representation in SBOM view
Browse files Browse the repository at this point in the history
Signed-off-by: Kaden Emley <[email protected]>
  • Loading branch information
kemley76 committed Aug 12, 2024
1 parent f5e469c commit 3becdd7
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 220 deletions.
38 changes: 24 additions & 14 deletions apps/frontend/src/components/cards/sbomview/ComponentContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,17 @@ import {Prop} from 'vue-property-decorator';
import _ from 'lodash';
import {ContextualizedControl} from 'inspecjs';
import {parseJson} from '@mitre/hdf-converters/src/utils/parseJson';
import {SBOMComponent, SBOMMetadata, SBOMProperty} from '@/utilities/sbom_util';
import {
ContextualizedSBOMComponent,
SBOMMetadata,
SBOMProperty
} from '@/utilities/sbom_util';
interface Tab {
name: string;
tableData?: TableData;
treeData?: Treeview[];
relatedComponents?: SBOMComponent[];
relatedComponents?: ContextualizedSBOMComponent[];
}
type TableData = {columns: string[]; rows: string[][]};
Expand Down Expand Up @@ -147,7 +151,7 @@ function objectToTreeview(obj: Object, id: number): Treeview[] {
*/
function objectDepth(obj: Object, n: number): number {
let max = n;
for (const [_key, value] of Object.entries(obj)) {
for (const value of Object.values(obj)) {
if (value instanceof Object) {
const current = objectDepth(value, n + 1);
if (current > max) max = current;
Expand Down Expand Up @@ -190,13 +194,20 @@ function jsonToTabFormat(value: Object): Omit<Tab, 'name'> {
}
/** A list of tabs that don't need to be auto-generated */
const customTabs = ['affectingVulnerabilities', 'properties', 'component'];
const customRender = [
'affectingVulnerabilities',
'properties',
'component',
'parents',
'children',
'key'
];
/**
* Generates a list of tabs to represent the given object
*/
function generateTabs(
object: SBOMComponent | SBOMMetadata,
object: ContextualizedSBOMComponent | SBOMMetadata,
prefix: string = ''
): Tab[] {
const tabs: Tab[] = [];
Expand All @@ -211,7 +222,7 @@ function generateTabs(
};
for (const [key, value] of Object.entries(object)) {
if (customTabs.includes(key) || key.startsWith('_')) continue;
if (customRender.includes(key)) continue;
if (value instanceof Object) {
tabs.push({
name: `${prefix}${_.startCase(key)}`,
Expand Down Expand Up @@ -267,15 +278,14 @@ function generateTabsFromProperties(
components: {}
})
export default class ComponentContent extends Vue {
@Prop({type: Object, required: true}) readonly component!: SBOMComponent;
@Prop({type: Object, required: true})
readonly component!: ContextualizedSBOMComponent;
// Describes metadata for an entire SBOM
@Prop({type: Object, required: false}) readonly metadata?: SBOMMetadata;
@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 Down Expand Up @@ -313,17 +323,17 @@ export default class ComponentContent extends Vue {
});
}
if (this.dependencies?.length) {
if (this.component.children.length) {
tabs.push({
name: 'Dependencies',
relatedComponents: this.dependencies
relatedComponents: this.component.children
});
}
if (this.parents?.length) {
if (this.component.parents.length) {
tabs.push({
name: 'Parents',
relatedComponents: this.parents
relatedComponents: this.component.parents
});
}
return tabs;
Expand Down
35 changes: 4 additions & 31 deletions apps/frontend/src/components/cards/sbomview/ComponentTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
:expanded.sync="expanded"
show-expand
:items-per-page="-1"
item-key="_key"
item-key="key"
hide-default-footer
>
<!-- fixed-header
Expand Down Expand Up @@ -105,8 +105,6 @@
<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"
/>
Expand All @@ -129,10 +127,7 @@ import {Prop} from 'vue-property-decorator';
import ComponentContent from './ComponentContent.vue';
import {
getVulnsFromBomRef,
SBOMComponent,
getStructuredSbomDependencies,
sbomDependencyToComponents,
SBOMDependency
ContextualizedSBOMComponent
} from '@/utilities/sbom_util';
@Component({
Expand All @@ -145,7 +140,7 @@ export default class ComponentTable extends Vue {
componentRef = this.$route.query.componentRef ?? null;
severityFilter: Severity[] = this.severities;
expanded: SBOMComponent[] = [];
expanded: ContextualizedSBOMComponent[] = [];
/** The list of columns that are currently displayed */
headerColumns = [
'name',
Expand Down Expand Up @@ -206,7 +201,7 @@ export default class ComponentTable extends Vue {
return h;
}
get components(): readonly SBOMComponent[] {
get components(): readonly ContextualizedSBOMComponent[] {
return FilteredDataModule.components(this.all_filter);
}
Expand All @@ -233,28 +228,6 @@ 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 Down
75 changes: 28 additions & 47 deletions apps/frontend/src/components/cards/sbomview/DependencyTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div>
<v-treeview
:items="loadedDependencies"
:load-children="getStructure"
:load-children="loadChildren"
dense
activatable
>
Expand All @@ -24,83 +24,64 @@
</template>

<script lang="ts">
import {FilteredDataModule} from '@/store/data_filters';
import {
getSbomMetadata,
SBOMDependency,
getStructuredSbomDependencies
} from '@/utilities/sbom_util';
import {ContextualizedSBOMComponent, SBOMData} 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[];
children?: TreeNode[];
id: number;
component: ContextualizedSBOMComponent;
}
type DependencyStructure = TreeNode & SBOMDependency;
@Component({
components: {}
})
export default class DependencyTree extends Vue {
@Prop({type: Object, required: true}) readonly sbomData!: SBOMData;
@Prop({type: String, required: false}) readonly searchTerm!: string;
@Prop({type: String, required: false}) readonly targetComponent!:
| string
| null;
loadedDependencies: DependencyStructure[] = [];
loadedDependencies: TreeNode[] = [];
nextId = 0;
mounted() {
this.loadedDependencies = this.rootComponentRefs.map(this.getRootStructure);
console.log('Mounted ' + this.targetComponent);
const root = this.sbomData.metadata?.component;
if (root) this.loadedDependencies = [this.componentToTreeNode(root)];
}
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', '') : ''
);
loadChildren(item: TreeNode) {
item.children = item.component.children.map(this.componentToTreeNode);
}
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: []};
componentToTreeNode(component: ContextualizedSBOMComponent): TreeNode {
return {
name: this.componentDisplayName(component),
id: this.nextId++, // use this.nextId and then increment it
component: component,
// having a children array indicates that children can be loaded
// the UI will reflect this
children: component.children?.length ? [] : undefined
};
}
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;
componentDisplayName(component: ContextualizedSBOMComponent): string {
const group = _.get(component, 'group');
const version = _.get(component, 'version');
const name = component.name;
if (group && version) return `${group}/${name} ${version}`;
if (group) return `${group}/${name}`;
if (version) return `${name} ${version}`;
return component.name;
}
}
Expand Down
28 changes: 11 additions & 17 deletions apps/frontend/src/store/data_filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import {
SourcedContextualizedProfile
} from '@/store/report_intake';
import Store from '@/store/store';
import {execution_unique_key} from '@/utilities/format_util';
import {
componentFitsSeverityFilter,
getSbomComponents,
isOnlySbomFileId,
isSbomFileId,
SBOMComponent
parseSbomPassthrough,
ContextualizedSBOMComponent
} from '@/utilities/sbom_util';
import {
ContextualizedControl,
Expand Down Expand Up @@ -514,25 +513,20 @@ export class FilteredData extends VuexModule {
};
}

get components(): (filter: Filter) => readonly SBOMComponent[] {
get components(): (filter: Filter) => readonly ContextualizedSBOMComponent[] {
return (filter: Filter) => {
const evaluations = this.sboms(filter.fromFile);
const components: SBOMComponent[] = [];
const sboms = this.sboms(filter.fromFile).map(parseSbomPassthrough);
const controls = this.controls(filter);

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

0 comments on commit 3becdd7

Please sign in to comment.