Skip to content

Commit 328b0ee

Browse files
xmunozewdurbin
authored andcommitted
Add rudimentary verdicts view. Progress on #6062. (#7207)
* Add rudimentary verdicts view. Progress on #6062. Also, add some better testing logic for wiped_out condition. * Code review changes. - Conditionally show fields that are populated - JSON pretty formatting * Fix unit test bug. - Use `get` instead of `filter` to look up verdict by pkey. * simplify unit tests for verdicts view
1 parent 046dbc1 commit 328b0ee

File tree

12 files changed

+350
-4
lines changed

12 files changed

+350
-4
lines changed

tests/common/db/malware.py

+6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ class Meta:
5151

5252
check = factory.SubFactory(MalwareCheckFactory)
5353
release_file = factory.SubFactory(FileFactory)
54+
release = None
55+
project = None
56+
manually_reviewed = True
57+
administrator_verdict = factory.fuzzy.FuzzyChoice(list(VerdictClassification))
5458
classification = factory.fuzzy.FuzzyChoice(list(VerdictClassification))
5559
confidence = factory.fuzzy.FuzzyChoice(list(VerdictConfidence))
5660
message = factory.fuzzy.FuzzyText(length=80)
61+
full_report_link = None
62+
details = None

tests/common/db/packaging.py

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class Meta:
8383

8484
release = factory.SubFactory(ReleaseFactory)
8585
python_version = "source"
86+
filename = factory.fuzzy.FuzzyText(length=12)
8687
md5_digest = factory.LazyAttribute(
8788
lambda o: hashlib.md5(o.filename.encode("utf8")).hexdigest()
8889
)

tests/unit/admin/test_routes.py

+4
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,8 @@ def test_includeme():
132132
"/admin/checks/{check_name}/change_state",
133133
domain=warehouse,
134134
),
135+
pretend.call("admin.verdicts.list", "/admin/verdicts/", domain=warehouse),
136+
pretend.call(
137+
"admin.verdicts.detail", "/admin/verdicts/{verdict_id}", domain=warehouse
138+
),
135139
]
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import uuid
14+
15+
from random import randint
16+
17+
import pretend
18+
import pytest
19+
20+
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound
21+
22+
from warehouse.admin.views import verdicts as views
23+
24+
from ....common.db.malware import MalwareVerdictFactory
25+
26+
27+
class TestListVerdicts:
28+
def test_none(self, db_request):
29+
assert views.get_verdicts(db_request) == {"verdicts": []}
30+
31+
def test_some(self, db_request):
32+
verdicts = [MalwareVerdictFactory.create() for _ in range(10)]
33+
34+
assert views.get_verdicts(db_request) == {"verdicts": verdicts}
35+
36+
def test_some_with_multipage(self, db_request):
37+
verdicts = [MalwareVerdictFactory.create() for _ in range(60)]
38+
39+
db_request.GET["page"] = "2"
40+
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+
46+
with pytest.raises(HTTPBadRequest):
47+
views.get_verdicts(request)
48+
49+
50+
class TestGetVerdict:
51+
def test_found(self, db_request):
52+
verdicts = [MalwareVerdictFactory.create() for _ in range(10)]
53+
index = randint(0, 9)
54+
lookup_id = verdicts[index].id
55+
db_request.matchdict["verdict_id"] = lookup_id
56+
57+
assert views.get_verdict(db_request) == {"verdict": verdicts[index]}
58+
59+
def test_not_found(self, db_request):
60+
db_request.matchdict["verdict_id"] = uuid.uuid4()
61+
62+
with pytest.raises(HTTPNotFound):
63+
views.get_verdict(db_request)

tests/unit/malware/test_tasks.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,14 @@ def test_no_verdicts(self, db_session):
266266
log=pretend.stub(info=pretend.call_recorder(lambda *args, **kwargs: None),),
267267
)
268268
task = pretend.stub()
269-
remove_verdicts(task, request, check.name)
269+
removed = remove_verdicts(task, request, check.name)
270270

271271
assert request.log.info.calls == [
272272
pretend.call(
273273
"Removing 0 malware verdicts associated with %s version 1." % check.name
274274
),
275275
]
276+
assert removed == 0
276277

277278
@pytest.mark.parametrize(("check_with_verdicts"), [True, False])
278279
def test_many_verdicts(self, db_session, check_with_verdicts):
@@ -286,6 +287,8 @@ def test_many_verdicts(self, db_session, check_with_verdicts):
286287
for i in range(num_verdicts):
287288
MalwareVerdictFactory.create(check=check1, release_file=file0)
288289

290+
assert db_session.query(MalwareVerdict).count() == num_verdicts
291+
289292
request = pretend.stub(
290293
db=db_session,
291294
log=pretend.stub(info=pretend.call_recorder(lambda *args, **kwargs: None),),
@@ -299,11 +302,13 @@ def test_many_verdicts(self, db_session, check_with_verdicts):
299302
wiped_out_check = check0
300303
num_verdicts = 0
301304

302-
remove_verdicts(task, request, wiped_out_check.name)
305+
removed = remove_verdicts(task, request, wiped_out_check.name)
303306

304307
assert request.log.info.calls == [
305308
pretend.call(
306309
"Removing %d malware verdicts associated with %s version 1."
307310
% (num_verdicts, wiped_out_check.name)
308311
),
309312
]
313+
314+
assert removed == num_verdicts

warehouse/admin/routes.py

+4
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,7 @@ def includeme(config):
139139
"/admin/checks/{check_name}/change_state",
140140
domain=warehouse,
141141
)
142+
config.add_route("admin.verdicts.list", "/admin/verdicts/", domain=warehouse)
143+
config.add_route(
144+
"admin.verdicts.detail", "/admin/verdicts/{verdict_id}", domain=warehouse
145+
)

warehouse/admin/templates/admin/base.html

+5
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@
130130
<i class="fa fa-check"></i> <span>Checks</span>
131131
</a>
132132
</li>
133+
<li>
134+
<a href="{{ request.route_path('admin.verdicts.list') }}">
135+
<i class="fa fa-gavel"></i> <span>Verdicts</span>
136+
</a>
137+
</li>
133138
</ul>
134139
</section>
135140
</aside>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
-#}
14+
{% extends "admin/base.html" %}
15+
16+
{% block title %}Verdict {{ verdict.id }}{% endblock %}
17+
18+
{% block breadcrumb %}
19+
<li><a href="{{ request.route_path('admin.verdicts.list') }}">Verdicts</a></li>
20+
<li class="active">{{ verdict.id }}</li>
21+
{% endblock %}
22+
23+
{% block content %}
24+
<div class="box box-primary">
25+
<div class="box-body box-profile">
26+
<table class="table table-hover">
27+
<tr>
28+
<th scope="row">Message</th>
29+
<td>{{ verdict.message }}</td>
30+
</tr>
31+
<tr>
32+
<th scope="row">Run Date</th>
33+
<td>{{ verdict.run_date }}</td>
34+
</tr>
35+
<tr>
36+
<th scope="row">Check</th>
37+
<td>
38+
<a href="{{ request.route_path('admin.checks.detail', check_name=verdict.check.name) }}">
39+
{{ verdict.check.name }} v{{ verdict.check.version }}
40+
</a>
41+
</td>
42+
</tr>
43+
<tr>
44+
<th scope="row">Object</th>
45+
<td>{% include 'object_link.html' %}</td>
46+
</tr>
47+
<tr>
48+
<th scope="row">Verdict Classification</th>
49+
<td>{{ verdict.classification.value }}</td>
50+
</tr>
51+
<tr>
52+
<th scope="row">Verdict Confidence</th>
53+
<td>{{ verdict.confidence.value }}</td>
54+
</tr>
55+
<tr>
56+
<th scope="row">Manually Reviewed</th>
57+
<td>{{ verdict.manually_reviewed }}</td>
58+
</tr>
59+
{% if verdict.manually_reviewed %}
60+
<tr>
61+
<th scope="row">Administrator Verdict</th>
62+
<td>{{ verdict.administrator_verdict }}</td>
63+
</tr>
64+
{% endif %}
65+
{% if verdict.full_report_link %}
66+
<tr>
67+
<th scope="row">Full Report Link</th>
68+
<td>{{ verdict.full_report_link }}</td>
69+
</tr>
70+
{% endif %}
71+
{% if verdict.details %}
72+
<tr>
73+
<th scope="row">Details</th>
74+
<td><pre>{{ verdict.details|tojson(indent=4) }}</pre></td>
75+
</tr>
76+
{% endif %}
77+
</table>
78+
</div>
79+
</div>
80+
{% endblock %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
-#}
14+
{% extends "admin/base.html" %}
15+
16+
{% import "admin/utils/pagination.html" as pagination %}
17+
18+
{% block title %}Malware Verdicts{% endblock %}
19+
20+
{% block breadcrumb %}
21+
<li class="active">Verdicts</li>
22+
{% endblock %}
23+
24+
{% block content %}
25+
<div class="box box-primary">
26+
<div class="box-body table-responsive no-padding">
27+
<table class="table table-hover">
28+
<tr>
29+
<th>Object</th>
30+
<th>Check</th>
31+
<th>Classification</th>
32+
<th>Confidence</th>
33+
<th>Detail</th>
34+
</tr>
35+
{% for verdict in verdicts %}
36+
<tr>
37+
<td>{% include 'object_link.html' %}</td>
38+
<td>
39+
<a href="{{ request.route_path('admin.checks.detail', check_name=verdict.check.name) }}">
40+
{{ verdict.check.name }} v{{ verdict.check.version }}
41+
</a>
42+
</td>
43+
<td>
44+
<span title="{{ verdict.classification.value }}">
45+
<i class="fa fa-exclamation"></i>
46+
{% if verdict.classification.value == 'indeterminate' %}
47+
<i class="fa fa-exclamation"></i>
48+
{% elif verdict.classification.value == 'threat' %}
49+
<i class="fa fa-exclamation"></i>
50+
<i class="fa fa-exclamation"></i>
51+
{% endif %}
52+
</span>
53+
</td>
54+
<td>
55+
<span title="{{ verdict.confidence.value }}">
56+
<i class="fa fa-star"></i>
57+
{% if verdict.confidence.value == 'medium' %}
58+
<i class="fa fa-star"></i>
59+
{% elif verdict.confidence.value == 'high' %}
60+
<i class="fa fa-star"></i>
61+
<i class="fa fa-star"></i>
62+
{% endif %}
63+
</span>
64+
</td>
65+
<td>
66+
<a href="{{ request.route_path('admin.verdicts.detail', verdict_id=verdict.id) }}">
67+
Detail
68+
</a>
69+
</td>
70+
</tr>
71+
{% else %}
72+
<tr>
73+
<td colspan="5">
74+
<center>
75+
<i>No verdicts!</i>
76+
</center>
77+
</td>
78+
</tr>
79+
{% endfor %}
80+
</table>
81+
<div class="box-footer">
82+
<div class="col-sm-5">
83+
{{ pagination.summary(verdicts) }}
84+
</div>
85+
<div class="col-sm-7">
86+
<div class="pull-right">
87+
{{ pagination.paginate(verdicts) }}
88+
</div>
89+
</div>
90+
</div>
91+
</div>
92+
</div>
93+
{% endblock content %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
-#}
14+
15+
{% if verdict.project %}
16+
<a href="{{ request.route_path('admin.project.detail', project_name=verdict.project.normalized_name) }}"><i class="fa fa-cube"></i> {{ verdict.project.name }} </a>
17+
{% elif verdict.release %}
18+
<a href="{{ request.route_path('admin.project.release', project_name=verdict.release.project.normalized_name, version=verdict.release.version) }}"><i class="far fa-folder"></i> {{ verdict.release.project.name }}-{{ verdict.release.version }} </a>
19+
{% else %}
20+
<a href="{{ request.route_path('admin.project.release', project_name=verdict.release_file.release.project.normalized_name, version=verdict.release_file.release.version) }}"><i class="far fa-file"></i> {{ verdict.release_file.filename}} </a>
21+
{% endif %}

0 commit comments

Comments
 (0)