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

Web-ui user view scan reports #4017

Merged
merged 15 commits into from
Apr 9, 2025
Merged
Show file tree
Hide file tree
Changes from 10 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@

/**
* This http request interceptor does a trace logging of all adapter
* communication when log level for this logger is set to trace. It uses the
* DEBUG log level from SLF4J to identify if the data shall be inspected and
* logged or not.<br>
* communication when log level for this logger is set to trace.<br>
* <br>
*
* @author Albert Tregnaghi
Expand All @@ -38,14 +36,7 @@ public final ClientHttpResponse intercept(HttpRequest request, byte[] body, Clie
}

private void log(HttpRequest request, byte[] body, ClientHttpResponse response) throws IOException {
if (!LOG.isDebugEnabled()) {
/*
* no output wanted - just do nothing and return. we use debug level, because
* this is the standard log level when executing as wiremock too - currently not
* able to change the log level for those tests because wiremock... does
* something special with logging in this case... If the wiremock logging issue
* could be changed then we should use a TRACE level instead!
*/
if (!LOG.isTraceEnabled()) {
return;
}
StringBuilder sb = new StringBuilder();
Expand Down
5 changes: 5 additions & 0 deletions sechub-openapi-java/src/main/resources/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,11 @@ components:
type: string
version:
type: string
headers:
type: object
additionalProperties: true
body:
$ref: '#/components/schemas/SecHubReportWebBody'

SecHubReportWebBodyLocation:
title: SecHubReportWebBodyLocation
Expand Down
3 changes: 3 additions & 0 deletions sechub-web-ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ node_modules
.env.local
.env.*.local

# Sechub reports
sechub_report_*

# Log files
npm-debug.log*
yarn-debug.log*
Expand Down
8 changes: 8 additions & 0 deletions sechub-web-ui/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ declare module 'vue' {
AppHeader: typeof import('./components/AppHeader.vue')['default']
AsyncButton: typeof import('./components/AsyncButton.vue')['default']
EmailVerificationSuccess: typeof import('./components/EmailVerificationSuccess.vue')['default']
JobReport: typeof import('./components/JobReport.vue')['default']
JobReportCodescanCallsRecursive: typeof import('./components/JobReportCodescanCallsRecursive.vue')['default']
JobReportCodescanDetails: typeof import('./components/JobReportCodescanDetails.vue')['default']
JobReportOverview: typeof import('./components/JobReportOverview.vue')['default']
JobReportStatus: typeof import('./components/JobReportStatus.vue')['default']
JobReportToolBar: typeof import('./components/JobReportToolBar.vue')['default']
JobReportWebscanDetails: typeof import('./components/JobReportWebscanDetails.vue')['default']
Pagination: typeof import('./components/Pagination.vue')['default']
ProjectDetails: typeof import('./components/ProjectDetails.vue')['default']
ProjectDetailsFab: typeof import('./components/ProjectDetailsFab.vue')['default']
ProjectJobList: typeof import('./components/ProjectJobList.vue')['default']
ProjectSettingsDialog: typeof import('./components/ProjectSettingsDialog.vue')['default']
ProjectsList: typeof import('./components/ProjectsList.vue')['default']
ProjectToolbar: typeof import('./components/ProjectToolbar.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScanCreate: typeof import('./components/ScanCreate.vue')['default']
Expand Down
212 changes: 212 additions & 0 deletions sechub-web-ui/src/components/JobReport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<!-- SPDX-License-Identifier: MIT -->
<template>

<JobReportToolBar
:job-u-u-i-d="jobUUID"
:project-id="projectId"
:scan-type="scantype"
:traffic-light="report.trafficLight || ''"
/>

<v-data-table
:group-by="groupBy"
:headers="headers"
item-key="id"
:items="sortedFindings"
show-expand
>

<template #group-header="{ item, columns, toggleGroup, isGroupOpen }">
<tr>
<td :colspan="columns.length">
<div class="d-flex align-center">
<v-btn
color="medium-emphasis"
density="comfortable"
:icon="isGroupOpen(item) ? '$expand' : '$next'"
size="small"
variant="outlined"
@click="toggleGroup(item)"
/>

<span class="ms-4">
<div>
<v-icon
class="ma-2"
:color="calculateColor(item.value)"
:icon="calculateIcon(item.value)"
left
/>
<span>{{ item.value }}</span>
</div>
</span>
</div>
</td>
</tr>
</template>

<template #item.severity="{ value }">
<div>
<v-icon
class="ma-2"
:color="calculateColor(value)"
:icon="calculateIcon(value)"
left
/>
<span>{{ value }}</span>
</div>
</template>

<template #item.cweId="{ value }">
<div>
<a :href="`https://cwe.mitre.org/data/definitions/${value}.html`">CWE-{{ value }}</a>
</div>
</template>

<template #item.data-table-expand="{ internalItem, isExpanded, toggleExpand }">
<v-btn
:append-icon="isExpanded(internalItem) ? 'mdi-chevron-up' : 'mdi-chevron-down'"
class="text-none"
color="primary"
:text="isExpanded(internalItem) ? $t('REPORT_COLLAPS_FINDING') : $t('REPORT_SHOW_FINDING')"
variant="text"
@click="toggleExpand(internalItem)"
/>
</template>

<template #expanded-row="{ columns, item }">
<tr>
<td class="py-2" :colspan="columns.length">
<v-sheet v-if="item.type !== 'webScan'" rounded="lg">
<JobReportCodescanDetails
:item="item"
/>
</v-sheet>
<v-sheet v-else rounded="lg">
<JobReportWebscanDetails
:item="item"
/>
</v-sheet>
</td>
</tr>
</template>
</v-data-table>
</template>
<script lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { SecHubFinding, SecHubReport } from '@/generated-sources/openapi'
import { useReportStore } from '@/stores/reportStore'
import '@/styles/sechub.scss'

export default {
name: 'JobReport',

setup () {
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const store = useReportStore()

const projectId = ref('')
const jobUUID = ref('')

const report = ref<SecHubReport>({})

const headers = [
{ title: 'ID', key: 'id', sortable: true },
{ title: t('REPORT_DESCRIPTION_SEVERITY'), key: 'severity' },
{ title: 'CWE', key: 'cweId' },
{ title: t('REPORT_DESCRIPTION_NAME'), key: 'name' },
]

const groupBy = ref([{ key: 'severity', order: false }])

if ('id' in route.params) {
projectId.value = route.params.id
}

if ('jobId' in route.params) {
jobUUID.value = route.params.jobId
}

const query = route.query.scantype as string
const scantype = ref('')
scantype.value = query

const filteredFindings = computed(() => {
if (report.value.result?.findings) {
return report.value.result?.findings.filter(finding => finding.type?.toLocaleLowerCase() === scantype.value) || []
} else {
return report.value.result?.findings
}
})

const severityOrder = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO']

const sortedFindings = computed<SecHubFinding[]>(() => {
if (!filteredFindings.value) {
return []
}
return [...filteredFindings.value].sort((a, b) => {
return severityOrder.indexOf(a.severity || 'INFO') - severityOrder.indexOf(b.severity || 'INFO')
})
})

onMounted(async () => {
const reportFromStore = store.getReportByUUID(jobUUID.value)
if (!reportFromStore) {
router.push({
path: '/projects',
})
} else {
report.value = reportFromStore
}
})

function calculateIcon (severity :string) {
switch (severity) {
case 'CRITICAL':
case 'HIGH':
return 'mdi-alert-circle-outline'
case 'MEDIUM':
return 'mdi-alert-circle-outline'
case 'LOW':
case 'INFO':
return 'mdi-information-outline'
default:
return 'mdi-help-circle'
}
}

function calculateColor (severity: string) {
switch (severity) {
case 'CRITICAL':
case 'HIGH':
return 'error'
case 'MEDIUM':
return 'warning'
case 'LOW':
return 'success'
case 'INFO':
return 'primary'
default:
return 'layer_01'
}
}

return {
projectId,
jobUUID,
report,
scantype,
headers,
groupBy,
calculateColor,
calculateIcon,
sortedFindings,
}
},
}

</script>
34 changes: 34 additions & 0 deletions sechub-web-ui/src/components/JobReportCodescanCallsRecursive.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<template v-if="call.calls">
<tr>
<td>{{ call.location }}</td>
<td>{{ call.line }}</td>
<td>{{ call.column }}</td>
<td>{{ call.relevantPart }}</td>
<td>{{ call.source }}</td>
</tr>
<JobReportCodescanCallsRecursive :call="call.calls" />
</template>
<template v-else>
<tr>
<td>{{ call.location }}</td>
<td>{{ call.line }}</td>
<td>{{ call.column }}</td>
<td>{{ call.relevantPart }}</td>
<td>{{ call.source }}</td>
</tr>
</template>
</template>
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
name: 'JobReportCodescanCallsRecursive',
props: {
call: {
type: Object,
required: true,
},
},
})
</script>
Loading
Loading