Skip to content

Commit 31b711c

Browse files
authored
Add verdicts view filtering capabilities #6062. (#7322)
* Add verdicts view filtering capabilities #6062. * Code review changes. - Refactor tests to be parametrized. - Pass `_query` to `route_path` in template. - Remove `is None` from filter query, it adds nothing.
1 parent 79ffb2f commit 31b711c

File tree

4 files changed

+249
-26
lines changed

4 files changed

+249
-26
lines changed

tests/unit/admin/views/test_verdicts.py

+151-12
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,176 @@
1414

1515
from random import randint
1616

17-
import pretend
1817
import pytest
1918

2019
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound
2120

2221
from warehouse.admin.views import verdicts as views
22+
from warehouse.malware.models import VerdictClassification, VerdictConfidence
2323

24-
from ....common.db.malware import MalwareVerdictFactory
24+
from ....common.db.malware import MalwareCheckFactory, MalwareVerdictFactory
2525

2626

2727
class TestListVerdicts:
2828
def test_none(self, db_request):
29-
assert views.get_verdicts(db_request) == {"verdicts": []}
29+
assert views.get_verdicts(db_request) == {
30+
"verdicts": [],
31+
"check_names": set(),
32+
"classifications": set(["threat", "indeterminate", "benign"]),
33+
"confidences": set(["low", "medium", "high"]),
34+
}
3035

3136
def test_some(self, db_request):
32-
verdicts = [MalwareVerdictFactory.create() for _ in range(10)]
37+
check = MalwareCheckFactory.create()
38+
verdicts = [MalwareVerdictFactory.create(check=check) for _ in range(10)]
3339

34-
assert views.get_verdicts(db_request) == {"verdicts": verdicts}
40+
assert views.get_verdicts(db_request) == {
41+
"verdicts": verdicts,
42+
"check_names": set([check.name]),
43+
"classifications": set(["threat", "indeterminate", "benign"]),
44+
"confidences": set(["low", "medium", "high"]),
45+
}
3546

3647
def test_some_with_multipage(self, db_request):
37-
verdicts = [MalwareVerdictFactory.create() for _ in range(60)]
48+
check1 = MalwareCheckFactory.create()
49+
check2 = MalwareCheckFactory.create()
50+
verdicts = [MalwareVerdictFactory.create(check=check2) for _ in range(60)]
3851

3952
db_request.GET["page"] = "2"
4053

41-
assert views.get_verdicts(db_request) == {"verdicts": verdicts[25:50]}
42-
43-
def test_with_invalid_page(self):
44-
request = pretend.stub(params={"page": "not an integer"})
45-
54+
assert views.get_verdicts(db_request) == {
55+
"verdicts": verdicts[25:50],
56+
"check_names": set([check1.name, check2.name]),
57+
"classifications": set(["threat", "indeterminate", "benign"]),
58+
"confidences": set(["low", "medium", "high"]),
59+
}
60+
61+
@pytest.mark.parametrize(
62+
"check_name", ["check0", "check1", ""],
63+
)
64+
def test_check_name_filter(self, db_request, check_name):
65+
result_verdicts, all_verdicts = [], []
66+
for i in range(3):
67+
check = MalwareCheckFactory.create(name="check%d" % i)
68+
verdicts = [MalwareVerdictFactory.create(check=check) for _ in range(5)]
69+
all_verdicts.extend(verdicts)
70+
if check.name == check_name:
71+
result_verdicts = verdicts
72+
73+
# Emptry string
74+
if not result_verdicts:
75+
result_verdicts = all_verdicts
76+
77+
response = {
78+
"verdicts": result_verdicts,
79+
"check_names": set(["check0", "check1", "check2"]),
80+
"classifications": set(["threat", "indeterminate", "benign"]),
81+
"confidences": set(["low", "medium", "high"]),
82+
}
83+
84+
db_request.GET["check_name"] = check_name
85+
assert views.get_verdicts(db_request) == response
86+
87+
@pytest.mark.parametrize(
88+
"classification", ["benign", "indeterminate", "threat", ""],
89+
)
90+
def test_classification_filter(self, db_request, classification):
91+
check1 = MalwareCheckFactory.create()
92+
result_verdicts, all_verdicts = [], []
93+
for c in VerdictClassification:
94+
verdicts = [
95+
MalwareVerdictFactory.create(check=check1, classification=c)
96+
for _ in range(5)
97+
]
98+
all_verdicts.extend(verdicts)
99+
if c.value == classification:
100+
result_verdicts = verdicts
101+
102+
# Emptry string
103+
if not result_verdicts:
104+
result_verdicts = all_verdicts
105+
106+
db_request.GET["classification"] = classification
107+
response = {
108+
"verdicts": result_verdicts,
109+
"check_names": set([check1.name]),
110+
"classifications": set(["threat", "indeterminate", "benign"]),
111+
"confidences": set(["low", "medium", "high"]),
112+
}
113+
assert views.get_verdicts(db_request) == response
114+
115+
@pytest.mark.parametrize(
116+
"confidence", ["low", "medium", "high", ""],
117+
)
118+
def test_confidence_filter(self, db_request, confidence):
119+
check1 = MalwareCheckFactory.create()
120+
result_verdicts, all_verdicts = [], []
121+
for c in VerdictConfidence:
122+
verdicts = [
123+
MalwareVerdictFactory.create(check=check1, confidence=c)
124+
for _ in range(5)
125+
]
126+
all_verdicts.extend(verdicts)
127+
if c.value == confidence:
128+
result_verdicts = verdicts
129+
130+
# Emptry string
131+
if not result_verdicts:
132+
result_verdicts = all_verdicts
133+
134+
response = {
135+
"verdicts": result_verdicts,
136+
"check_names": set([check1.name]),
137+
"classifications": set(["threat", "indeterminate", "benign"]),
138+
"confidences": set(["low", "medium", "high"]),
139+
}
140+
141+
db_request.GET["confidence"] = confidence
142+
assert views.get_verdicts(db_request) == response
143+
144+
@pytest.mark.parametrize(
145+
"manually_reviewed", [1, 0],
146+
)
147+
def test_manually_reviewed_filter(self, db_request, manually_reviewed):
148+
check1 = MalwareCheckFactory.create()
149+
result_verdicts = [
150+
MalwareVerdictFactory.create(
151+
check=check1, manually_reviewed=bool(manually_reviewed)
152+
)
153+
for _ in range(5)
154+
]
155+
156+
# Create other verdicts to ensure filter works properly
157+
for _ in range(10):
158+
MalwareVerdictFactory.create(
159+
check=check1, manually_reviewed=not bool(manually_reviewed)
160+
)
161+
162+
db_request.GET["manually_reviewed"] = str(manually_reviewed)
163+
164+
response = {
165+
"verdicts": result_verdicts,
166+
"check_names": set([check1.name]),
167+
"classifications": set(["threat", "indeterminate", "benign"]),
168+
"confidences": set(["low", "medium", "high"]),
169+
}
170+
171+
assert views.get_verdicts(db_request) == response
172+
173+
@pytest.mark.parametrize(
174+
"invalid_param",
175+
[
176+
("page", "invalid"),
177+
("check_name", "NotACheck"),
178+
("confidence", "NotAConfidence"),
179+
("classification", "NotAClassification"),
180+
("manually_reviewed", "False"),
181+
],
182+
)
183+
def test_errors(self, db_request, invalid_param):
184+
db_request.GET[invalid_param[0]] = invalid_param[1]
46185
with pytest.raises(HTTPBadRequest):
47-
views.get_verdicts(request)
186+
views.get_verdicts(db_request)
48187

49188

50189
class TestGetVerdict:

warehouse/admin/templates/admin/base.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@
131131
</a>
132132
</li>
133133
<li>
134-
<a href="{{ request.route_path('admin.verdicts.list') }}">
134+
<a href="{{ request.route_path('admin.verdicts.list', _query={'classification':'threat', 'manually_reviewed':'0'}) }}">
135135
<i class="fa fa-gavel"></i> <span>Verdicts</span>
136136
</a>
137137
</li>

warehouse/admin/templates/admin/malware/verdicts/index.html

+42-1
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,61 @@
1717

1818
{% block title %}Malware Verdicts{% endblock %}
1919

20+
{% set check_name = request.params.get("check_name") %}
21+
{% set classification = request.params.get("classification") %}
22+
{% set confidence = request.params.get("confidence") %}
23+
{% set manually_reviewed = request.params.get("manually_reviewed") %}
24+
2025
{% block breadcrumb %}
2126
<li class="active">Verdicts</li>
2227
{% endblock %}
2328

2429
{% block content %}
2530
<div class="box box-primary">
31+
<div class="box-header with-border">
32+
<form class="form-inline">
33+
<div class="form-group">
34+
<select class="form-control" name="check_name">
35+
<option {{ "selected" if check_name is none else "" }} disabled value="">Check Name</option>
36+
{% for c in check_names %}
37+
<option {{ "selected" if check_name == c else "" }}>{{c}}</option>
38+
{% endfor %}
39+
</select>
40+
</div>
41+
<div class="form-group">
42+
<select class="form-control" name="classification">
43+
<option {{ "selected" if classification is none else "" }} disabled value="">Classification</option>
44+
{% for c in classifications %}
45+
<option {{ "selected" if classification == c else "" }}>{{c}}</option>
46+
{% endfor %}
47+
</select>
48+
</div>
49+
<div class="form-group">
50+
<select class="form-control" name="confidence">
51+
<option {{ "selected" if confidence is none else "" }} disabled value="">Confidence</option>
52+
{% for c in confidences %}
53+
<option {{ "selected" if confidence == c else "" }}>{{c}}</option>
54+
{% endfor %}
55+
</select>
56+
</div>
57+
<div class="form-group">
58+
<select class="form-control" name="manually_reviewed">
59+
<option {{ "selected" if manually_reviewed is none else "" }} disabled value="">Needs Review?</option>
60+
<option {{ "selected" if manually_reviewed == "0" else "" }} value="0">Needs Review</option>
61+
<option {{ "selected" if manually_reviewed == "1" else "" }} value="1">Already Reviewed</option>
62+
</select>
63+
</div>
64+
<button type="submit" class="btn btn-primary">Filter</button>
65+
</form>
66+
</div>
2667
<div class="box-body table-responsive no-padding">
2768
<table class="table table-hover">
2869
<tr>
2970
<th>Object</th>
3071
<th>Check</th>
3172
<th>Classification</th>
3273
<th>Confidence</th>
33-
<th>Detail</th>
74+
<th>Review</th>
3475
</tr>
3576
{% for verdict in verdicts %}
3677
<tr>

warehouse/admin/views/verdicts.py

+55-12
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound
1515
from pyramid.view import view_config
1616

17-
from warehouse.malware.models import MalwareVerdict
17+
from warehouse.malware.models import (
18+
MalwareCheck,
19+
MalwareVerdict,
20+
VerdictClassification,
21+
VerdictConfidence,
22+
)
1823
from warehouse.utils.paginate import paginate_url_factory
1924

2025

@@ -26,23 +31,23 @@
2631
uses_session=True,
2732
)
2833
def get_verdicts(request):
29-
try:
30-
page_num = int(request.params.get("page", 1))
31-
except ValueError:
32-
raise HTTPBadRequest("'page' must be an integer.") from None
33-
34-
verdicts_query = request.db.query(MalwareVerdict).order_by(
35-
MalwareVerdict.run_date.desc()
34+
result = {}
35+
result["check_names"] = set(
36+
[name for (name,) in request.db.query(MalwareCheck.name)]
3637
)
38+
result["classifications"] = set([c.value for c in VerdictClassification])
39+
result["confidences"] = set([c.value for c in VerdictConfidence])
40+
41+
validate_fields(request, result)
3742

38-
verdicts = SQLAlchemyORMPage(
39-
verdicts_query,
40-
page=page_num,
43+
result["verdicts"] = SQLAlchemyORMPage(
44+
generate_query(request.db, request.params),
45+
page=int(request.params.get("page", 1)),
4146
items_per_page=25,
4247
url_maker=paginate_url_factory(request),
4348
)
4449

45-
return {"verdicts": verdicts}
50+
return result
4651

4752

4853
@view_config(
@@ -59,3 +64,41 @@ def get_verdict(request):
5964
return {"verdict": verdict}
6065

6166
raise HTTPNotFound
67+
68+
69+
def validate_fields(request, validators):
70+
try:
71+
int(request.params.get("page", 1))
72+
except ValueError:
73+
raise HTTPBadRequest("'page' must be an integer.") from None
74+
75+
validators = {**validators, **{"manually_revieweds": set(["0", "1"])}}
76+
77+
for key, possible_values in validators.items():
78+
# Remove the trailing 's'
79+
value = request.params.get(key[:-1])
80+
additional_values = set([None, ""])
81+
if value not in possible_values | additional_values:
82+
raise HTTPBadRequest(
83+
"Invalid value for '%s': %s." % (key[:-1], value)
84+
) from None
85+
86+
87+
def generate_query(db, params):
88+
"""
89+
Returns an SQLAlchemy query wth request params applied as filters.
90+
"""
91+
query = db.query(MalwareVerdict)
92+
if params.get("check_name"):
93+
query = query.join(MalwareCheck)
94+
query = query.filter(MalwareCheck.name == params["check_name"])
95+
if params.get("confidence"):
96+
query = query.filter(MalwareVerdict.confidence == params["confidence"])
97+
if params.get("classification"):
98+
query = query.filter(MalwareVerdict.classification == params["classification"])
99+
if params.get("manually_reviewed"):
100+
query = query.filter(
101+
MalwareVerdict.manually_reviewed == bool(int(params["manually_reviewed"]))
102+
)
103+
104+
return query.order_by(MalwareVerdict.run_date.desc())

0 commit comments

Comments
 (0)