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

2225564 metrics audits by auditor #117

Merged
merged 1 commit into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions components/resc-backend/src/resc_backend/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
RWS_ROUTE_RULE_PACKS = "/rule-packs"
RWS_ROUTE_VCS = "/vcs-instances"


RWS_ROUTE_AUDIT_COUNT_BY_AUDITOR_OVER_TIME = "/audit-count-by-auditor-over-time"
RWS_ROUTE_AUDITED_COUNT_OVER_TIME = "/audited-count-over-time"
RWS_ROUTE_UN_TRIAGED_COUNT_OVER_TIME = "/un-triaged-count-over-time"
RWS_ROUTE_COUNT_BY_TIME = "/count-by-time"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# pylint: disable=R0916,R0912,C0121
# Standard Library
import logging
from datetime import datetime
from datetime import datetime, timedelta

# Third Party
from sqlalchemy import func
from sqlalchemy import extract, func
from sqlalchemy.engine import Row
from sqlalchemy.orm import Session

# First Party
Expand Down Expand Up @@ -78,6 +79,34 @@ def get_finding_audits_count(db_connection: Session, finding_id: int) -> int:
:return: total_count
count of audit entries
"""
total_count = db_connection.query(func.count(model.DBaudit.id_))\
total_count = db_connection.query(func.count(model.DBaudit.id_)) \
.filter(model.DBaudit.finding_id == finding_id).scalar()
return total_count


def get_audit_count_by_auditor_over_time(db_connection: Session, weeks: int = 13) -> list[Row]:
"""
Retrieve count audits by auditor over time for given weeks
:param db_connection:
Session of the database connection
:param weeks:
optional, filter on last n weeks, default 13
:return: count_over_time
list of rows containing audit count over time per week
"""
last_nth_week_date_time = datetime.utcnow() - timedelta(weeks=weeks)

query = db_connection.query(extract('year', model.DBaudit.timestamp).label("year"),
extract('week', model.DBaudit.timestamp).label("week"),
model.DBaudit.auditor,
func.count(model.DBaudit.id_).label("audit_count")) \
.filter(model.DBaudit.timestamp >= last_nth_week_date_time) \
.group_by(extract('year', model.DBaudit.timestamp).label("year"),
extract('week', model.DBaudit.timestamp).label("week"),
model.DBaudit.auditor) \
.order_by(extract('year', model.DBaudit.timestamp).label("year"),
extract('week', model.DBaudit.timestamp).label("week"),
model.DBaudit.auditor)
finding_audits = query.all()

return finding_audits
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@
ERROR_MESSAGE_500,
ERROR_MESSAGE_503,
METRICS_TAG,
RWS_ROUTE_AUDIT_COUNT_BY_AUDITOR_OVER_TIME,
RWS_ROUTE_AUDITED_COUNT_OVER_TIME,
RWS_ROUTE_COUNT_PER_VCS_PROVIDER_BY_WEEK,
RWS_ROUTE_METRICS,
RWS_ROUTE_UN_TRIAGED_COUNT_OVER_TIME
)
from resc_backend.db.connection import Session
from resc_backend.resc_web_service.crud import audit as audit_crud
from resc_backend.resc_web_service.crud import finding as finding_crud
from resc_backend.resc_web_service.dependencies import get_db_connection
from resc_backend.resc_web_service.schema.audit_count_over_time import AuditCountOverTime
from resc_backend.resc_web_service.schema.finding_count_over_time import FindingCountOverTime
from resc_backend.resc_web_service.schema.finding_status import FindingStatus
from resc_backend.resc_web_service.schema.vcs_provider import VCSProviders
Expand Down Expand Up @@ -142,3 +145,48 @@ def convert_rows_to_finding_count_over_time(count_over_time: dict, weeks: int) -

output.append(week_data)
return output


@router.get(f"{RWS_ROUTE_AUDIT_COUNT_BY_AUDITOR_OVER_TIME}",
response_model=list[AuditCountOverTime],
summary="Get count of Audits by Auditor over time for given weeks",
status_code=status.HTTP_200_OK,
responses={
200: {"description": "Retrieve count of Audits by Auditor over time for given weeks"},
500: {"description": ERROR_MESSAGE_500},
503: {"description": ERROR_MESSAGE_503}
})
def get_audit_count_by_auditor_over_time(db_connection: Session = Depends(get_db_connection),
weeks: Optional[int] = Query(default=13, ge=1)) \
-> list[AuditCountOverTime]:
"""
Retrieve count of Audits by Auditor over time for given weeks
- **db_connection**: Session of the database connection
- **weeks**: Nr of weeks for which to retrieve the audit counts
- **return**: [AuditCountOverTime]
The output will contain a list of AuditCountOverTime type objects
"""
audit_counts = audit_crud.get_audit_count_by_auditor_over_time(db_connection=db_connection, weeks=weeks)

# get the unique auditors from the data
auditors_default = {}
for audit in audit_counts:
auditors_default[audit['auditor']] = 0

# default to 0 per auditor for all weeks in range
weekly_audit_counts = {}
for week in range(0, weeks):
nth_week = datetime.utcnow() - timedelta(weeks=week)
week = f"{nth_week.isocalendar().year} W{nth_week.isocalendar().week:02d}"
weekly_audit_counts[week] = AuditCountOverTime(time_period=week, audit_by_auditor_count=dict(auditors_default))
weekly_audit_counts = dict(sorted(weekly_audit_counts.items()))

# set the counts based on the data from the database
for audit in audit_counts:
audit_week = f"{audit['year']} W{audit['week']:02d}"
weekly_audit_counts.get(audit_week).audit_by_auditor_count[audit['auditor']] = audit['audit_count']
weekly_audit_counts.get(audit_week).total += audit['audit_count']

sorted_weekly_audit_counts = dict(sorted(weekly_audit_counts.items()))
output = list(sorted_weekly_audit_counts.values())
return output
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# pylint: disable=no-name-in-module
# Standard Library
from typing import Dict

# Third Party
from pydantic import BaseModel


class AuditCountOverTime(BaseModel):
time_period: str
audit_by_auditor_count: Dict[str, int]
total: int = 0
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "3d5d6abf-2f53-4200-b744-d3965c492658",
"_postman_id": "6db9b02e-2513-4d4e-a31d-c90f3a766832",
"name": "Repository Scanner (RESC)",
"description": "RESC API helps you to perform several operations upon findings obtained from multiple source code repositories. 🚀",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
Expand Down Expand Up @@ -18815,6 +18815,55 @@
"body": "[\n {\n \"time_period\": \"sed cupidatat dolore commodo\",\n \"vcs_provider_finding_count\": {\n \"AZURE_DEVOPS\": 0,\n \"BITBUCKET\": 0,\n \"GITHUB_PUBLIC\": 0\n },\n \"total\": 0\n },\n {\n \"time_period\": \"veniam\",\n \"vcs_provider_finding_count\": {\n \"AZURE_DEVOPS\": 0,\n \"BITBUCKET\": 0,\n \"GITHUB_PUBLIC\": 0\n },\n \"total\": 0\n }\n]"
}
]
},
{
"name": "Get count of auditds by auditer over time for given weeks",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Status is OK\"), function (){",
" pm.response.to.have.property(\"status\",\" OK\")",
"}",
"",
"const responseJson = pm.response.json();",
"pm.test(\"Response body matches\", function() {",
" pm.expect(responseJson).to.be.an(\"array\");",
" pm.expect(responseJson.length).to.eql(13);",
" responseJson.forEach((obj) => {",
" pm.expect(obj).to.have.property('time_period');",
" pm.expect(obj).to.have.property('audit_by_auditor_count');",
" pm.expect(obj).to.have.property('total');",
" });",
" ",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/resc/v1/metrics/audit-count-by-auditor-over-time",
"host": [
"{{baseUrl}}"
],
"path": [
"resc",
"v1",
"metrics",
"audit-count-by-auditor-over-time"
]
}
},
"response": []
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

# First Party
from resc_backend.constants import (
RWS_ROUTE_AUDIT_COUNT_BY_AUDITOR_OVER_TIME,
RWS_ROUTE_AUDITED_COUNT_OVER_TIME,
RWS_ROUTE_COUNT_PER_VCS_PROVIDER_BY_WEEK,
RWS_ROUTE_METRICS,
Expand Down Expand Up @@ -90,3 +91,18 @@ def test_convert_rows_to_finding_count_over_time(self):
assert data[1].total == data2["finding_count"]+data3["finding_count"]
assert data[0].time_period == f"{third_week.isocalendar().year} W{third_week.isocalendar().week:02d}"
assert data[0].total == 0

@patch("resc_backend.resc_web_service.crud.audit.get_audit_count_by_auditor_over_time")
def test_get_audit_count_by_auditor_over_time(self, get_audit_count_by_auditor_over_time):
get_audit_count_by_auditor_over_time.return_value = {}
response = self.client.get(f"{RWS_VERSION_PREFIX}{RWS_ROUTE_METRICS}"
f"{RWS_ROUTE_AUDIT_COUNT_BY_AUDITOR_OVER_TIME}")

assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 13
first_week = datetime.utcnow() - timedelta(weeks=0)
nth_week = datetime.utcnow() - timedelta(weeks=len(data)-1)
assert data[len(data)-1]["time_period"] == f"{first_week.isocalendar().year} " \
f"W{first_week.isocalendar().week:02d}"
assert data[0]["time_period"] == f"{nth_week.isocalendar().year} W{nth_week.isocalendar().week:02d}"
105 changes: 105 additions & 0 deletions components/resc-frontend/src/components/Metrics/AuditMetrics.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<template>
<div>
<div class="col-md-2 pt-2 text-left page-title">
<h3><small class="text-nowrap">Audit Metrics</small></h3>
</div>
<div class="pl-2">
<div class="row">
<div class="col-md-5 pt-2">
<h5><small class="text-nowrap">Audits by Auditor per week</small></h5>
<Spinner :active="!loadedAuditCounts" />
<MultiLineChart
v-if="loadedAuditCounts"
:chart-data="chartDataForAuditCountsGraph"
:chart-options="chartOptions"
/>
</div>
</div>
</div>
</div>
</template>

<script>
import AxiosConfig from '@/configuration/axios-config.js';
import FindingsService from '@/services/findings-service';
import MultiLineChart from '@/components/Charts/MultiLineChart.vue';
import Spinner from '@/components/Common/Spinner.vue';

export default {
name: 'AuditMetrics',
components: {
MultiLineChart,
Spinner,
},
data() {
return {
loadedAuditCounts: false,
loadedAuditCountsAuditors: false,
chartDataForAuditCountsGraph: { labels: [], datasets: [] },
auditCounts: [],
chartOptions: {
responsive: true,
maintainAspectRatio: false,
},
};
},
methods: {
arrayContainsAllZeros(arr) {
return arr.every((item) => item === 0);
},
getGraphData() {
FindingsService.getAuditsByAuditorPerWeek()
.then((response) => {
this.auditCounts = response.data;
let datasets = {};
this.auditCounts.forEach((data) => {
this.chartDataForAuditCountsGraph['labels'].push(data.time_period);

if (!this.loadedAuditCountsAuditors) {
Object.entries(data.audit_by_auditor_count).forEach((auditorData) => {
datasets[auditorData[0]] = this.prepareDataSet(auditorData[0], auditorData[1]);
});
datasets['Total'] = this.prepareDataSet('Total', data.total);
this.loadedAuditCountsAuditors = true;
} else {
Object.entries(data.audit_by_auditor_count).forEach((auditorData) => {
datasets[auditorData[0]].data.push(auditorData[1]);
});
datasets['Total'].data.push(data.total);
}
});
Object.entries(datasets).forEach((auditorDataset) => {
this.chartDataForAuditCountsGraph.datasets.push(auditorDataset[1]);
});
this.loadedAuditCounts = true;
})
.catch((error) => {
AxiosConfig.handleError(error);
});
},
prepareDataSet(datasetLabel, datasetFirstValue) {
const datasetsObj = {};
let colourCode = '#' + Math.floor(Math.random() * 16777215).toString(16);
datasetsObj.borderWidth = 1.5;
datasetsObj.cubicInterpolationMode = 'monotone';
datasetsObj.data = [datasetFirstValue];
datasetsObj.pointStyle = 'circle';
datasetsObj.pointRadius = 3;
datasetsObj.pointHoverRadius = 8;
datasetsObj.label = datasetLabel;

if (datasetLabel === 'Total') {
datasetsObj.hidden = true;
}
datasetsObj.borderColor = colourCode;
datasetsObj.pointBackgroundColor = colourCode;
datasetsObj.backgroundColor = colourCode;

return datasetsObj;
},
},
mounted() {
this.getGraphData();
},
};
</script>
14 changes: 12 additions & 2 deletions components/resc-frontend/src/components/Navigation/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export default {
},
child: [
{
href: '/metrics/rule-metrics',
title: 'Rule Metrics',
href: '/metrics/audit-metrics',
title: 'Audit Metrics',
icon: {
element: 'font-awesome-icon',
attributes: {
Expand All @@ -36,6 +36,16 @@ export default {
},
},
},
{
href: '/metrics/rule-metrics',
title: 'Rule Metrics',
icon: {
element: 'font-awesome-icon',
attributes: {
icon: 'chart-bar',
},
},
},
],
},
{
Expand Down
6 changes: 6 additions & 0 deletions components/resc-frontend/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import Config from '@/configuration/config';
import FindingMetrics from '@/components/Metrics/FindingMetrics';
import AuditMetrics from '@/components/Metrics/AuditMetrics';
import Store from '@/store/index.js';
import Analytics from '@/views/Analytics';
import Repositories from '@/views/Repositories';
Expand Down Expand Up @@ -49,6 +50,11 @@ const routes = [
name: 'FindingMetrics',
component: FindingMetrics,
},
{
path: '/metrics/audit-metrics',
name: 'AuditMetrics',
component: AuditMetrics,
},
{
path: '/rulepacks',
name: 'RulePacks',
Expand Down
3 changes: 3 additions & 0 deletions components/resc-frontend/src/services/findings-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ const FindingsService = {
async getTruePositiveCountPerVcsProviderPerWeek() {
return axios.get(`/metrics/audited-count-over-time`);
},
async getAuditsByAuditorPerWeek() {
return axios.get(`/metrics/audit-count-by-auditor-over-time`);
},

async getFindingAudits(findingId, perPage, skipRowCount) {
return axios.get(`findings/${findingId}/audit`, {
Expand Down
Loading