Skip to content

Commit 0666dc7

Browse files
authored
Merge pull request #219 from Xpirix/qa_security_checks
QA & Security checks for plugins
2 parents 6c69c73 + 4e43299 commit 0666dc7

File tree

15 files changed

+2099
-25
lines changed

15 files changed

+2099
-25
lines changed

dockerize/docker/REQUIREMENTS.txt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,17 @@ sentry-sdk~=2.2
5555
django-webpack-loader~=3.1
5656

5757
beautifulsoup4~=4.12
58-
setuptools~=75.1
58+
setuptools~=75.1
59+
60+
# Security scanning tools for QGIS Plugin analysis (invoked via subprocess)
61+
# Bandit - Security vulnerability scanner for Python code
62+
bandit~=1.9
63+
64+
# detect-secrets - Finds hardcoded secrets in code
65+
detect-secrets~=1.5
66+
67+
# flake8 for code quality
68+
flake8~=7.3
69+
70+
# flake8-json for structured output (optional but recommended)
71+
flake8-json~=24.4

qgis-app/docs.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,14 @@ def docs_faq(request):
3333
"flatpages/docs_faq.html",
3434
{},
3535
)
36+
37+
38+
def docs_security_scanning(request):
39+
"""
40+
Renders the docs_security_scanning page
41+
"""
42+
return render(
43+
request,
44+
"flatpages/docs_security_scanning.html",
45+
{},
46+
)

qgis-app/plugins/admin.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from django.contrib import admin
2-
from plugins.models import Plugin, PluginVersion, PluginVersionDownload # , PluginCrashReport
2+
from plugins.models import ( # , PluginCrashReport
3+
Plugin,
4+
PluginVersion,
5+
PluginVersionDownload,
6+
PluginVersionSecurityScan,
7+
)
38

49

510
class PluginAdmin(admin.ModelAdmin):
@@ -29,14 +34,58 @@ class PluginVersionAdmin(admin.ModelAdmin):
2934

3035

3136
class PluginVersionDownloadAdmin(admin.ModelAdmin):
37+
list_display = ("plugin_version", "download_date", "download_count")
38+
raw_id_fields = ("plugin_version",)
39+
40+
41+
class PluginVersionSecurityScanAdmin(admin.ModelAdmin):
3242
list_display = (
3343
"plugin_version",
34-
"download_date",
35-
"download_count"
44+
"scanned_on",
45+
"overall_status",
46+
"pass_rate",
47+
"total_checks",
48+
"passed_checks",
49+
"critical_count",
50+
"warning_count",
3651
)
37-
raw_id_fields = (
52+
list_filter = ("scanned_on",)
53+
search_fields = ("plugin_version__plugin__name", "plugin_version__version")
54+
readonly_fields = (
3855
"plugin_version",
56+
"scanned_on",
57+
"total_checks",
58+
"passed_checks",
59+
"warning_count",
60+
"critical_count",
61+
"info_count",
62+
"files_scanned",
63+
"total_issues",
64+
"scan_report",
3965
)
66+
raw_id_fields = ()
67+
68+
def overall_status(self, obj):
69+
status = obj.overall_status
70+
colors = {
71+
"passed": "green",
72+
"info": "blue",
73+
"warning": "orange",
74+
"critical": "red",
75+
}
76+
return f'<span style="color: {colors.get(status, "black")}; font-weight: bold;">{status.upper()}</span>'
77+
78+
overall_status.allow_tags = True
79+
overall_status.short_description = "Status"
80+
81+
def pass_rate(self, obj):
82+
rate = obj.pass_rate
83+
color = "green" if rate >= 80 else "orange" if rate >= 60 else "red"
84+
return f'<span style="color: {color}; font-weight: bold;">{rate}%</span>'
85+
86+
pass_rate.allow_tags = True
87+
pass_rate.short_description = "Pass Rate"
88+
4089

4190
# class PluginCrashReportAdmin(admin.ModelAdmin):
4291
# pass
@@ -45,4 +94,5 @@ class PluginVersionDownloadAdmin(admin.ModelAdmin):
4594
admin.site.register(Plugin, PluginAdmin)
4695
admin.site.register(PluginVersion, PluginVersionAdmin)
4796
admin.site.register(PluginVersionDownload, PluginVersionDownloadAdmin)
97+
admin.site.register(PluginVersionSecurityScan, PluginVersionSecurityScanAdmin)
4898
# admin.site.register(PluginCrashReport, PluginCrashReportAdmin)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Generated by Django 4.2.27 on 2026-01-13 05:24
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("plugins", "0017_merge_20260109_0103"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="PluginVersionSecurityScan",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True,
21+
primary_key=True,
22+
serialize=False,
23+
verbose_name="ID",
24+
),
25+
),
26+
(
27+
"scanned_on",
28+
models.DateTimeField(auto_now_add=True, verbose_name="Scanned on"),
29+
),
30+
(
31+
"total_checks",
32+
models.IntegerField(default=0, verbose_name="Total checks"),
33+
),
34+
(
35+
"passed_checks",
36+
models.IntegerField(default=0, verbose_name="Passed checks"),
37+
),
38+
(
39+
"warning_count",
40+
models.IntegerField(default=0, verbose_name="Warnings"),
41+
),
42+
(
43+
"critical_count",
44+
models.IntegerField(default=0, verbose_name="Critical issues"),
45+
),
46+
(
47+
"info_count",
48+
models.IntegerField(default=0, verbose_name="Info items"),
49+
),
50+
(
51+
"files_scanned",
52+
models.IntegerField(default=0, verbose_name="Files scanned"),
53+
),
54+
(
55+
"total_issues",
56+
models.IntegerField(default=0, verbose_name="Total issues"),
57+
),
58+
(
59+
"scan_report",
60+
models.JSONField(
61+
blank=True,
62+
default=dict,
63+
help_text="Complete scan report with all check details",
64+
verbose_name="Scan report",
65+
),
66+
),
67+
(
68+
"plugin_version",
69+
models.OneToOneField(
70+
on_delete=django.db.models.deletion.CASCADE,
71+
related_name="security_scan",
72+
to="plugins.pluginversion",
73+
),
74+
),
75+
],
76+
options={
77+
"verbose_name": "Plugin Version Security Scan",
78+
"verbose_name_plural": "Plugin Version Security Scans",
79+
"ordering": ["-scanned_on"],
80+
},
81+
),
82+
]

qgis-app/plugins/models.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,64 @@ class Meta:
11821182
)
11831183

11841184

1185+
class PluginVersionSecurityScan(models.Model):
1186+
"""
1187+
Security and quality scan results for plugin versions
1188+
Stores non-blocking security, quality, and code analysis results
1189+
"""
1190+
1191+
plugin_version = models.OneToOneField(
1192+
PluginVersion, on_delete=models.CASCADE, related_name="security_scan"
1193+
)
1194+
scanned_on = models.DateTimeField(
1195+
_("Scanned on"), auto_now_add=True, editable=False
1196+
)
1197+
1198+
# Summary statistics
1199+
total_checks = models.IntegerField(_("Total checks"), default=0)
1200+
passed_checks = models.IntegerField(_("Passed checks"), default=0)
1201+
warning_count = models.IntegerField(_("Warnings"), default=0)
1202+
critical_count = models.IntegerField(_("Critical issues"), default=0)
1203+
info_count = models.IntegerField(_("Info items"), default=0)
1204+
files_scanned = models.IntegerField(_("Files scanned"), default=0)
1205+
total_issues = models.IntegerField(_("Total issues"), default=0)
1206+
1207+
# Full scan report (JSON field)
1208+
scan_report = models.JSONField(
1209+
_("Scan report"),
1210+
default=dict,
1211+
blank=True,
1212+
help_text=_("Complete scan report with all check details"),
1213+
)
1214+
1215+
class Meta:
1216+
verbose_name = _("Plugin Version Security Scan")
1217+
verbose_name_plural = _("Plugin Version Security Scans")
1218+
ordering = ["-scanned_on"]
1219+
1220+
def __str__(self):
1221+
return f"Security scan for {self.plugin_version} ({self.scanned_on})"
1222+
1223+
@property
1224+
def overall_status(self):
1225+
"""Returns overall scan status"""
1226+
if self.critical_count > 0:
1227+
return "critical"
1228+
elif self.warning_count > 0:
1229+
return "warning"
1230+
elif self.info_count > 0:
1231+
return "info"
1232+
else:
1233+
return "passed"
1234+
1235+
@property
1236+
def pass_rate(self):
1237+
"""Calculate percentage of passed checks"""
1238+
if self.total_checks == 0:
1239+
return 0
1240+
return round((self.passed_checks / self.total_checks) * 100, 1)
1241+
1242+
11851243
models.signals.post_delete.connect(delete_version_package, sender=PluginVersion)
11861244
models.signals.post_delete.connect(delete_plugin_icon, sender=Plugin)
11871245
models.signals.post_delete.connect(

0 commit comments

Comments
 (0)