Skip to content

Commit dffb8fb

Browse files
Merge pull request #40 from DigitalProductInnovationAndDevelopment/feat/recommendationFilter
Feat/recommendationFilter
2 parents 5e3aa45 + 537e777 commit dffb8fb

16 files changed

+260
-90
lines changed

dashboard/src/app/components/findings-input-filter-dialog/findings-input-filter-dialog.component.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,16 @@ export class FindingsInputFilterDialogComponent {
5555
const priMaxValue = this.rangeFilters.priorities.maxValue;
5656
this.dialogRef.close({
5757
...((sevMinValue !== 0 || sevMaxValue !== 100) && {
58-
severity: [
59-
this.rangeFilters.severities.minValue,
60-
this.rangeFilters.severities.maxValue,
61-
],
58+
severity: {
59+
minValue: this.rangeFilters.severities.minValue,
60+
maxValue: this.rangeFilters.severities.maxValue,
61+
},
6262
}),
6363
...((priMinValue !== 0 || priMaxValue !== 100) && {
64-
priority: [
65-
this.rangeFilters.priorities.minValue,
66-
this.rangeFilters.priorities.maxValue,
67-
],
64+
priority: {
65+
minValue: this.rangeFilters.priorities.minValue,
66+
maxValue: this.rangeFilters.priorities.maxValue,
67+
},
6868
}),
6969
...(source && source.length > 0 && { source }),
7070
...(cve_ids && cve_ids.length > 0 && { cve_ids }),

dashboard/src/app/pages/results/results.component.html

+14-4
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,20 @@ <h2>Recommendations</h2>
1818
<span *ngIf="fileName$ | async as fileName">{{ fileName }}</span>
1919
</div>
2020
</div>
21-
<div class="info-container__numberFindings">
22-
<span>{{ findings?.length }}</span>
23-
<p>Vulnerabilities</p>
24-
<i class="bi bi-shield-exclamation"></i>
21+
<div class="info-container__right">
22+
<div class="slider">
23+
<span>{{ severityMinValue }}</span>
24+
<mat-slider min="0" max="100" step="1" (input)="filterRecommendations()">
25+
<input matSliderStartThumb [(ngModel)]="severityMinValue" />
26+
<input matSliderEndThumb [(ngModel)]="severityMaxValue" />
27+
</mat-slider>
28+
<span>{{ severityMaxValue }}</span>
29+
</div>
30+
<div class="info-container__numberFindings">
31+
<span>{{ findings?.length }}</span>
32+
<p>Vulnerabilities</p>
33+
<i class="bi bi-shield-exclamation"></i>
34+
</div>
2535
</div>
2636
</div>
2737

dashboard/src/app/pages/results/results.component.scss

+19
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,25 @@
2828
justify-content: space-between;
2929
margin-bottom: 20px;
3030

31+
&__right {
32+
display: flex;
33+
gap: 20px;
34+
align-items: center;
35+
.slider {
36+
color: #ffffff;
37+
display: flex;
38+
align-items: center;
39+
gap: 10px;
40+
::ng-deep {
41+
&.mat-mdc-slider {
42+
.mdc-slider__thumb-knob,
43+
.mdc-slider__track--active_fill {
44+
border-color: var(--textColorSecondary) !important;
45+
}
46+
}
47+
}
48+
}
49+
}
3150
&__selectedFile {
3251
display: flex;
3352
flex-direction: column;

dashboard/src/app/pages/results/results.component.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Component, OnDestroy, OnInit } from '@angular/core';
22
import { Router } from '@angular/router';
33
import { Select, Store } from '@ngxs/store';
4-
import { Observable, Subscription } from 'rxjs';
4+
import { Observable } from 'rxjs';
55
import { IFinding } from 'src/app/interfaces/IFinding';
6-
import { RecommendationsService } from 'src/app/services/recommendations.service';
76
import {
87
clearFindings,
8+
filterRecs,
99
loadRecommendations,
1010
} from 'src/app/states/recommendations.actions';
1111
import { RecommendationsState } from 'src/app/states/recommendations.state';
@@ -31,13 +31,10 @@ export class ResultsComponent implements OnInit, OnDestroy {
3131
@Select(RecommendationsState.exampleProcess)
3232
exampleProcess$!: Observable<boolean>;
3333
public loading: boolean = false;
34-
private subscription!: Subscription;
34+
public severityMinValue = 0;
35+
public severityMaxValue = 100;
3536

36-
constructor(
37-
private router: Router,
38-
private recommendationService: RecommendationsService,
39-
private store: Store
40-
) {}
37+
constructor(private router: Router, private store: Store) {}
4138

4239
ngOnInit(): void {
4340
this.getRecommendations();
@@ -57,7 +54,16 @@ export class ResultsComponent implements OnInit, OnDestroy {
5754
if (!fileName) {
5855
this.router.navigate(['home']);
5956
} else if (!exampleProcess) {
60-
this.store.dispatch(new loadRecommendations()).subscribe();
57+
this.store.dispatch(new loadRecommendations({})).subscribe();
6158
}
6259
}
60+
61+
public filterRecommendations(): void {
62+
console.log(this.severityMinValue, this.severityMaxValue);
63+
this.store.dispatch(
64+
new filterRecs({
65+
severity: [this.severityMinValue, this.severityMaxValue],
66+
})
67+
);
68+
}
6369
}

dashboard/src/app/services/recommendations.service.ts

+25-9
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ export class RecommendationsService {
1414
return this.http.get<void>(environment.apiUrl + '/');
1515
}
1616

17-
public uploadFindings(inputData: string, filter: any): Observable<number> {
17+
public uploadFindings(
18+
inputData: string,
19+
filter: any
20+
): Observable<{ task_id: number }> {
1821
return this.http
19-
.post<number>(environment.apiUrl + '/upload', {
22+
.post<any>(environment.apiUrl + '/upload', {
2023
data: inputData,
2124
user_id: 1,
2225
filter,
@@ -28,25 +31,38 @@ export class RecommendationsService {
2831
error.error.detail ===
2932
'Recommendation task already exists for today'
3033
) {
31-
return new Observable<number>((observer) => {
32-
observer.next(100);
34+
return new Observable<{ task_id: number }>((observer) => {
35+
observer.next({ task_id: 100 });
3336
observer.complete();
3437
});
3538
} else {
36-
return of(-1);
39+
return of({ task_id: -1 });
3740
}
3841
})
3942
);
4043
}
4144

42-
public getRecommendations(): Observable<ReceivedRecommendations> {
45+
public getRecommendations(
46+
taskId?: number,
47+
severity?: number[]
48+
): Observable<ReceivedRecommendations> {
49+
const body: any = {};
50+
if (taskId !== undefined) {
51+
body.taskId = taskId;
52+
}
53+
if (severity !== undefined) {
54+
body.severity = { minValue: severity[0], maxValue: severity[1] };
55+
}
56+
4357
return this.http.post<ReceivedRecommendations>(
4458
environment.apiUrl + '/recommendations',
45-
{}
59+
body
4660
);
4761
}
4862

49-
public getUploadStatus(): Observable<{ status: string }> {
50-
return this.http.get<{ status: string }>(environment.apiUrl + '/status');
63+
public getUploadStatus(taskId: number): Observable<{ status: string }> {
64+
return this.http.get<{ status: string }>(
65+
environment.apiUrl + `/tasks/${taskId}/status`
66+
);
5167
}
5268
}

dashboard/src/app/states/recommendations.actions.ts

+16
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,20 @@ export class clearFindings {
2727

2828
export class loadRecommendations {
2929
static readonly type = '[] load recommendations';
30+
31+
constructor(
32+
public readonly payload: {
33+
severity?: number[];
34+
}
35+
) {}
36+
}
37+
38+
export class filterRecs {
39+
static readonly type = '[severity] filter recommendations';
40+
41+
constructor(
42+
public readonly payload: {
43+
severity: number[];
44+
}
45+
) {}
3046
}

dashboard/src/app/states/recommendations.state.ts

+41-14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { IFinding } from 'src/app/interfaces/IFinding';
1414
import { RecommendationsService } from '../services/recommendations.service';
1515
import {
1616
clearFindings,
17+
filterRecs,
1718
loadRecommendations,
1819
setInformation,
1920
UploadFile,
@@ -25,6 +26,7 @@ export interface RecommendationsStateModel {
2526
findings: IFinding[];
2627
fileName: string | undefined;
2728
exampleProcess: boolean;
29+
taskId: number | undefined;
2830
}
2931

3032
@State<RecommendationsStateModel>({
@@ -35,6 +37,7 @@ export interface RecommendationsStateModel {
3537
findings: [],
3638
fileName: undefined,
3739
exampleProcess: false,
40+
taskId: undefined,
3841
},
3942
})
4043
@Injectable()
@@ -82,12 +85,12 @@ export class RecommendationsState {
8285
context: StateContext<RecommendationsStateModel>,
8386
{ payload }: UploadFile
8487
): Observable<number> {
85-
console.log(payload.filter);
8688
return this.recommendationService
8789
.uploadFindings(payload.data, payload.filter)
8890
.pipe(
89-
filter((response) => response !== -1),
90-
map((response) => response),
91+
filter((response) => response.task_id !== -1),
92+
tap((response) => context.patchState({ taskId: response.task_id })),
93+
map((response) => response.task_id),
9194
finalize(() => void context.patchState({ isLoading: false }))
9295
);
9396
}
@@ -102,16 +105,40 @@ export class RecommendationsState {
102105
}
103106

104107
@Action(loadRecommendations)
105-
loadRecommendations(context: StateContext<RecommendationsStateModel>): void {
106-
interval(10000)
107-
.pipe(
108-
switchMap(() => this.recommendationService.getUploadStatus()),
109-
takeWhile((response) => response.status !== 'completed', true),
110-
filter((response) => response.status === 'completed'),
111-
switchMap(() => this.recommendationService.getRecommendations()),
112-
tap((findings) => context.patchState({ findings: findings.items }))
113-
)
114-
.subscribe();
108+
loadRecommendations(
109+
context: StateContext<RecommendationsStateModel>,
110+
{ payload }: loadRecommendations
111+
): void {
112+
const taskId = context.getState().taskId;
113+
if (typeof taskId === 'number') {
114+
interval(10000)
115+
.pipe(
116+
filter(() => taskId !== undefined),
117+
switchMap(() => this.recommendationService.getUploadStatus(taskId)),
118+
takeWhile((response) => response.status !== 'completed', true),
119+
filter((response) => response.status === 'completed'),
120+
switchMap(() =>
121+
this.recommendationService.getRecommendations(
122+
taskId,
123+
payload.severity
124+
)
125+
),
126+
tap((findings) => context.patchState({ findings: findings.items }))
127+
)
128+
.subscribe();
129+
}
130+
}
131+
@Action(filterRecs)
132+
filterRecs(
133+
context: StateContext<RecommendationsStateModel>,
134+
{ payload }: filterRecs
135+
) {
136+
let findings = context.getState().findings;
137+
findings = findings.filter(
138+
(finding) =>
139+
finding.severity >= payload.severity[0] &&
140+
finding.severity <= payload.severity[1]
141+
);
142+
context.patchState({ findings });
115143
}
116144
}
117-

src/app.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ def run_migrations():
2929
async def lifespan(app_: FastAPI):
3030
log.info("Starting up...")
3131
log.info("run alembic upgrade head...")
32-
run_migrations()
32+
# run_migrations()
33+
log.info("alembic upgrade head done")
3334
yield
3435
log.info("Shutting down...")
3536

3637

37-
app = FastAPI()
38+
app = FastAPI(lifespan=lifespan)
3839

3940

4041
app.add_middleware(

src/data/apischema.py

+9-15
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
1-
from typing import List, Literal, Optional
1+
from typing import Any, Dict, List, Literal, Optional
22

33
from pydantic import BaseModel, validator
44

5+
from data.Categories import Category
56
from data.Finding import Finding
67
from data.pagination import Pagination, PaginationInput
8+
from data.Solution import Solution
79
from data.types import InputData
810
from db.models import TaskStatus
911

1012

13+
class SeverityFilter(BaseModel):
14+
minValue: int
15+
maxValue: int
1116
class FindingInputFilter(BaseModel):
12-
severity: Optional[List[int]] = None # ['low', 'high']
13-
priority: Optional[List[int]] = None # ['low', 'high']
17+
severity: Optional[SeverityFilter] = None # ['low', 'high']
18+
priority: Optional[SeverityFilter] = None # ['low', 'high']
1419
cve_ids: Optional[List[str]] = None
1520
cwe_ids: Optional[List[str]] = None
1621
source: Optional[List[str]] = None
17-
18-
@validator('severity', pre=True, always=True)
19-
def check_severity(cls, value):
20-
if value is not None and len(value) != 2:
21-
raise ValueError('severity must be an array with exactly 2 elements')
22-
return value
23-
@validator('priority', pre=True, always=True)
24-
def check_priority(cls, value):
25-
if value is not None and len(value) != 2:
26-
raise ValueError('priority must be an array with exactly 2 elements')
27-
return value
2822

2923
class StartRecommendationTaskRequest(BaseModel):
3024
user_id: Optional[int] = None
@@ -42,7 +36,7 @@ class GetRecommendationFilter(BaseModel):
4236
task_id: Optional[int] = None
4337
date: Optional[str] = None
4438
location: Optional[str] = None
45-
severity: Optional[str] = None
39+
severity: Optional[SeverityFilter] = None # ['low', 'high']
4640
cve_id: Optional[str] = None
4741
source: Optional[str] = None
4842

src/data/helper.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ def matches(content: Content) -> bool:
2727
if filter.source and not any(title.source in filter.source for title in content.title_list):
2828
return False
2929

30-
if filter.severity and not (filter.severity[0] <= content.severity <= filter.severity[1]):
30+
if filter.severity and not (filter.severity.minValue <= content.severity <= filter.severity.maxValue):
3131
return False
3232

33-
if filter.priority and not (filter.priority[0] <= content.priority <= filter.priority[1]):
33+
if filter.priority and not (filter.priority.minValue <= content.priority <= filter.priority.maxValue):
3434
return False
3535

3636
if filter.cve_ids and not any(cve.element in filter.cve_ids for cve in content.cve_id_list):

0 commit comments

Comments
 (0)