Skip to content

Commit bdfdfa0

Browse files
committed
Merge remote-tracking branch 'origin/feat/4-add-frontend'
2 parents f3ba37c + abc5085 commit bdfdfa0

File tree

9 files changed

+354
-6
lines changed

9 files changed

+354
-6
lines changed

docker-compose-dev.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ services:
3434
build:
3535
context: ./server
3636
dockerfile: Dockerfile
37-
command: celery -A tasks worker -l info -B
37+
command: celery -b redis://redis:6379/0 -A tasks worker -l info -B -s /tmp/celerybeat-schedule
3838
restart: always
3939
environment:
40-
- TASK_TRIVY_INTERVAL=1
41-
- TASK_XEOL_INTERVAL=1
40+
- TASK_TRIVY_INTERVAL=100
41+
- TASK_XEOL_INTERVAL=100
4242
- TASK_DATA_COLLECTOR_INTERVAL=1
4343
- CELERY_BROKER_URL=redis://redis:6379/0
4444
- CELERY_RESULT_BACKEND=redis://redis:6379/0
@@ -50,4 +50,4 @@ services:
5050
redis:
5151
image: redis:7-alpine
5252
ports:
53-
- "127.0.0.1:6379:6379"
53+
- "127.0.0.1:6379:6379"

server/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from config import Config
44
from extensions import db, migrate
55
from routes.general import general
6+
from routes.frontend import frontend
67

78

89
def create_app(config_object=Config):
@@ -23,6 +24,7 @@ def register_extensions(app):
2324

2425
def register_blueprints(app):
2526
app.register_blueprint(general)
27+
app.register_blueprint(frontend)
2628
return None
2729

2830

server/routes/frontend.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import json
2+
from dataclasses import dataclass
3+
4+
from flask import render_template, Blueprint
5+
6+
from models.host import Host
7+
from models.image import Image
8+
9+
frontend = Blueprint('frontend', __name__)
10+
11+
12+
def count_eol_images(host):
13+
eol_images_set = set() # To ensure each image is counted only once
14+
for container in host.containers:
15+
if is_image_eol(container.image):
16+
eol_images_set.add(container.image.id) # Use image.id as the unique identifier
17+
return len(eol_images_set)
18+
19+
def is_image_eol(image):
20+
if not image.status_xeol:
21+
return False
22+
status_xeol = image.status_xeol
23+
matches = status_xeol.get("matches", [])
24+
return any("Eol" in match["Cycle"] for match in matches)
25+
26+
27+
@dataclass
28+
class TrivyFindings:
29+
high: int
30+
medium: int
31+
low: int
32+
33+
def count_trivy_findings(host) -> TrivyFindings:
34+
trivy_findings = TrivyFindings(0, 0, 0)
35+
for container in host.containers:
36+
image = container.image
37+
if image and image.status_trivy:
38+
image_findings = count_trivy_findings_image(image)
39+
trivy_findings.high += image_findings.high
40+
trivy_findings.medium += image_findings.medium
41+
trivy_findings.low += image_findings.low
42+
return trivy_findings
43+
44+
45+
def count_trivy_findings_image(image):
46+
if not image or not image.status_trivy:
47+
return TrivyFindings(0, 0, 0)
48+
trivy_findings = TrivyFindings(0, 0, 0)
49+
status_trivy = image.status_trivy
50+
results = status_trivy["Results"]
51+
for result in results:
52+
if "Vulnerabilities" in result:
53+
for vulnerability in result["Vulnerabilities"]:
54+
severity = vulnerability["Severity"]
55+
if severity == "HIGH":
56+
trivy_findings.high += 1
57+
elif severity == "MEDIUM":
58+
trivy_findings.medium += 1
59+
elif severity == "LOW":
60+
trivy_findings.low += 1
61+
return trivy_findings
62+
63+
64+
@frontend.route('/hosts', methods=['GET'])
65+
def get_hosts():
66+
hosts = Host.query.all()
67+
68+
data = []
69+
for host in hosts:
70+
last_call = host.updated_at or host.created_at
71+
docker_containers = len(host.containers)
72+
docker_images = len(set(container.image_id for container in host.containers))
73+
eol_images = count_eol_images(host)
74+
trivy_findings = count_trivy_findings(host)
75+
76+
data.append({
77+
'id': host.id,
78+
'name': host.name,
79+
'last_successful_call': last_call,
80+
'docker_containers': docker_containers,
81+
'docker_images': docker_images,
82+
'eol_images': eol_images,
83+
'trivy_findings': trivy_findings,
84+
})
85+
86+
return render_template('hosts.html', data=data)
87+
88+
89+
@frontend.route('/hosts/<int:host_id>', methods=['GET'])
90+
def get_host(host_id):
91+
host = Host.query.get(host_id)
92+
if not host:
93+
return "Host not found", 404
94+
data = {
95+
'host': {
96+
'id': host.id,
97+
'name': host.name,
98+
},
99+
'containers': [],
100+
}
101+
for container in host.containers:
102+
image = container.image
103+
data['containers'].append({
104+
'name': container.name,
105+
'image': container.image_string,
106+
'image_hash': container.image.repo_digest,
107+
'image_eol': is_image_eol(image),
108+
'trivy_findings': count_trivy_findings_image(image),
109+
})
110+
111+
return render_template('host.html', data=data)
112+
113+
114+
@frontend.route('/images', methods=['GET'])
115+
def get_images():
116+
images = Image.query.all()
117+
118+
data = []
119+
for image in images:
120+
is_eol = is_image_eol(image)
121+
trivy_findings = count_trivy_findings_image(image)
122+
usage_count = len(image.containers)
123+
124+
data.append({
125+
'name': image.name,
126+
'image_eol': is_eol,
127+
'trivy_findings': trivy_findings,
128+
'usage_count': usage_count,
129+
})
130+
131+
return render_template('images.html', data=data)

server/routes/general.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from flask import jsonify, Blueprint
1+
from flask import jsonify, Blueprint, render_template
22
from sqlalchemy.sql import text
33
from extensions import db
44
import subprocess
@@ -11,7 +11,7 @@
1111

1212
@general.route('/', methods=['GET'])
1313
def get_home():
14-
return "there is nothing to see yet, please use the cli (see readme)! :)"
14+
return render_template("index.html")
1515

1616

1717
@general.route('/health', methods=['GET'])

server/templates/base.html.j2

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<!DOCTYPE html>
2+
<html lang="en" data-theme="night">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport"
6+
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
7+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
8+
<title>DockerInv{% block title %}{% endblock %}</title>
9+
<link rel="icon" href="/favicon.ico" type="image/x-icon">
10+
11+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css"/>
12+
<script src="https://cdn.tailwindcss.com"></script>
13+
{% block headers %}
14+
{% endblock %}
15+
<style>
16+
html, body {
17+
padding: 0;
18+
margin: 0;
19+
}
20+
</style>
21+
</head>
22+
<body>
23+
<div class="navbar bg-base-100 bg-secondary-content">
24+
<div class="flex-1">
25+
<a class="btn btn-ghost text-xl" href="/">DockerInv</a>
26+
</div>
27+
<div class="navbar-center text-info">
28+
{{ self.page() }}
29+
</div>
30+
<div class="flex-none navbar-end">
31+
<ul class="menu menu-horizontal px-1">
32+
<li><a href="/images">Images</a></li>
33+
<li><a href="/hosts">Hosts</a></li>
34+
</ul>
35+
<div class="form-control">
36+
<input type="text" placeholder="Host, Image, Container" class="input input-bordered w-24 md:w-auto"/>
37+
</div>
38+
</div>
39+
</div>
40+
41+
<div class="m-8">
42+
{% block body %}
43+
{% endblock %}
44+
</div>
45+
46+
<div class="fixed bottom-0 left-0 p-4">
47+
<label class="swap swap-rotate">
48+
<!-- this hidden checkbox controls the state -->
49+
<input type="checkbox" class="theme-controller" value="light"/>
50+
51+
<!-- sun icon -->
52+
<svg
53+
class="swap-off h-10 w-10 fill-current"
54+
xmlns="http://www.w3.org/2000/svg"
55+
viewBox="0 0 24 24">
56+
<path
57+
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"></path>
58+
</svg>
59+
60+
<!-- moon icon -->
61+
<svg
62+
class="swap-on h-10 w-10 fill-current"
63+
xmlns="http://www.w3.org/2000/svg"
64+
viewBox="0 0 24 24">
65+
<path
66+
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"></path>
67+
</svg>
68+
</label>
69+
</div>
70+
{% block scripts %}
71+
{% endblock %}
72+
</body>
73+
</html>

server/templates/host.html

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{% extends 'base.html.j2' %}
2+
3+
{% block page %}{{ data.host.name }}{% endblock %}
4+
5+
{% block body %}
6+
7+
<table class="table table-zebra">
8+
<thead>
9+
<tr>
10+
<th>Container Name</th>
11+
<th>Image Name / Tag</th>
12+
<th>Result of Xeol</th>
13+
<th>Result of Trivy</th>
14+
</tr>
15+
</thead>
16+
<tbody>
17+
{% for container in data.containers %}
18+
<tr class="hover">
19+
<td>{{ container.name }}</td>
20+
<td>
21+
<div class="tooltip" data-tip="{{ container.image_hash }}">
22+
{{ container.image }}
23+
</div>
24+
</td>
25+
<td>
26+
{% if container.image_eol %}
27+
<span class="text-error">EOL</span>
28+
{% else %}
29+
<span class="text-success"></span>
30+
{% endif %}
31+
</td>
32+
<td>
33+
<span class="text-error">{{ container.trivy_findings['high'] }}</span> |
34+
<span class="text-warning">{{ container.trivy_findings['medium'] }}</span> |
35+
<span class="text-info">{{ container.trivy_findings['low'] }}</span>
36+
</td>
37+
</tr>
38+
{% endfor %}
39+
</tbody>
40+
</table>
41+
{% endblock %}
42+
43+
{% block scripts %}
44+
<script>
45+
function openHost(id) {
46+
window.location.href = '/hosts/' + id;
47+
}
48+
</script>
49+
{% endblock %}

server/templates/hosts.html

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{% extends 'base.html.j2' %}
2+
3+
{% block page %}Hosts{% endblock %}
4+
5+
{% block body %}
6+
<table class="table table-zebra">
7+
<thead>
8+
<tr>
9+
<th>Name</th>
10+
<th>Last Successful Agent Call</th>
11+
<th>Docker Containers</th>
12+
<th>Docker Images</th>
13+
<th>End-of-Life Images</th>
14+
<th>Aggregated Trivy Findings</th>
15+
</tr>
16+
</thead>
17+
<tbody>
18+
{% for entry in data %}
19+
<tr class="hover" onclick="openHost('{{ entry.id }}')">
20+
<td>{{ entry.name }}</td>
21+
<td>{{ entry.last_successful_call }}</td>
22+
<td>{{ entry.docker_containers }}</td>
23+
<td>{{ entry.docker_images }}</td>
24+
<td>{{ entry.eol_images }}</td>
25+
<td>
26+
<span class="text-error">{{ entry.trivy_findings['high'] }}</span> |
27+
<span class="text-warning">{{ entry.trivy_findings['medium'] }}</span> |
28+
<span class="text-info">{{ entry.trivy_findings['low'] }}</span>
29+
</td>
30+
</tr>
31+
{% endfor %}
32+
</tbody>
33+
</table>
34+
{% endblock %}
35+
36+
{% block scripts %}
37+
<script>
38+
function openHost(id) {
39+
window.location.href = "/hosts/" + id;
40+
}
41+
</script>
42+
{% endblock %}

server/templates/images.html

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{% extends 'base.html.j2' %}
2+
3+
{% block page %}Images{% endblock %}
4+
5+
{% block body %}
6+
<table class="table table-zebra">
7+
<thead>
8+
<tr>
9+
<th>Image Name</th>
10+
<th>Result of XEOL</th>
11+
<th>Result of Trivy</th>
12+
<th>Usage by Containers</th>
13+
</tr>
14+
</thead>
15+
<tbody>
16+
{% for image in data %}
17+
<tr class="hover" onclick="openImage('{{ image.id }}')">
18+
<td>{{ image.name }}</td>
19+
<td>
20+
{% if image.image_eol %}
21+
<span class="text-error">EOL</span>
22+
{% else %}
23+
<span class="text-success"></span>
24+
{% endif %}
25+
</td>
26+
<td>
27+
<span class="text-error">{{ image.trivy_findings['high'] }}</span> |
28+
<span class="text-warning">{{ image.trivy_findings['medium'] }}</span> |
29+
<span class="text-info">{{ image.trivy_findings['low'] }}</span>
30+
</td>
31+
<td>{{ image.usage_count }}</td>
32+
</tr>
33+
{% endfor %}
34+
</tbody>
35+
</table>
36+
{% endblock %}
37+
38+
{% block scripts %}
39+
<script>
40+
function openImage(id) {
41+
window.location.href = "/images/" + id;
42+
}
43+
</script>
44+
{% endblock %}

0 commit comments

Comments
 (0)