Skip to content

Commit d3dc869

Browse files
committed
Add target_info and tool_settings fields to ScanJob model; update ScanJobSerializer to exclude tool settings; enhance ScanConfigurationManager.vue with new tool configuration options for Semgrep and Trivy, including validation and parsing logic.
1 parent 0370244 commit d3dc869

5 files changed

Lines changed: 245 additions & 43 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.0.14 on 2025-05-14 17:49
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('core', '0008_project_project_main_targets_json_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='scanjob',
15+
name='target_info',
16+
field=models.JSONField(blank=True, help_text='The specific target information used for this scan job.', null=True),
17+
),
18+
migrations.AddField(
19+
model_name='scanjob',
20+
name='tool_settings',
21+
field=models.JSONField(blank=True, help_text='The specific tool settings used for this scan job.', null=True),
22+
),
23+
]

backend/core/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ class ScanJob(models.Model):
172172
ci_build_id = models.CharField(max_length=255, null=True, blank=True, db_index=True)
173173
triggered_by_ci = models.BooleanField(default=False)
174174

175+
# Fields to store the actual target and tool configurations for this specific job run
176+
target_info = models.JSONField(null=True, blank=True, help_text="The specific target information used for this scan job.")
177+
tool_settings = models.JSONField(null=True, blank=True, help_text="The specific tool settings used for this scan job.")
178+
175179
def __str__(self):
176180
return f"Scan Job {self.id} for {self.project.name} - Status: {self.status}"
177181

backend/core/serializers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,7 @@ class Meta:
164164
'celery_task_id',
165165
'initiated_by',
166166
'created_at', 'started_timestamp', 'completed_timestamp',
167-
'results',
168-
'target_info', 'tool_settings'
167+
'results'
169168
]
170169

171170
def validate(self, data):

backend/core/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def get_permissions(self):
5757
def perform_create(self, serializer):
5858
"""Ensure the user creating the project is set as its owner and create a default scan configuration."""
5959
project = serializer.save(owner=self.request.user)
60-
ProjectMembership.objects.create(user=self.request.user, project=project, role=ProjectMembership.Role.OWNER) # Corrected role to use Enum
60+
ProjectMembership.objects.create(user=self.request.user, project=project, role=ProjectMembership.Role.MANAGER) # Corrected role to MANAGER
6161

6262
# Automatically create a default ScanConfiguration for the new project
6363
default_config_name = f"Default Configuration for {project.name}"

frontend/src/components/ScanConfigurationManager.vue

Lines changed: 216 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,60 @@
6868
<textarea id="config-target-details" v-model="configForm.target_details_json" rows="5" placeholder='e.g., {\n "type": "git_repo",\n "url": "https://github.com/user/repo.git",\n "branch": "main",\n "include_paths": ["src/"],\n "exclude_paths": ["tests/"]\n}'></textarea>
6969
<small>Specify targets if 'has_predefined_targets' is checked. Otherwise, manual input will be required during scan run.</small>
7070
</div>
71-
<div class="form-group">
72-
<label for="config-tool-settings">Tool Configurations (JSON):</label>
73-
<textarea id="config-tool-settings" v-model="configForm.tool_configurations_json" rows="5" placeholder='e.g., {\n "bandit": { "enabled": true, "severity_level": "MEDIUM" },\n "semgrep": { "enabled": true, "rulesets": ["p/default"] }\n}'></textarea>
74-
<small>Define tool-specific settings. If empty, default tool behavior will apply.</small>
75-
</div>
76-
<div v-if="formErrorMessage" class="error-message">{{ formErrorMessage }}</div>
71+
72+
<!-- New Tool Configuration Section -->
73+
<fieldset class="tool-config-fieldset">
74+
<legend>Tool Configurations</legend>
75+
76+
<!-- Semgrep -->
77+
<div class="tool-config-group">
78+
<label class="tool-enable-label">
79+
<input type="checkbox" v-model="configForm.tools.semgrep.enabled">
80+
Enable Semgrep
81+
</label>
82+
<div v-if="configForm.tools.semgrep.enabled" class="tool-options">
83+
<label for="semgrep-rulesets">Semgrep Rulesets (kommagetrennt):</label>
84+
<input type="text" id="semgrep-rulesets" v-model="configForm.tools.semgrep.rulesets" placeholder="z.B. p/ci,r/generic">
85+
<small>Standard: community-recommended (oft 'p/ci' oder leer lassen für Standard)</small>
86+
</div>
87+
</div>
88+
89+
<!-- Trivy -->
90+
<div class="tool-config-group">
91+
<label class="tool-enable-label">
92+
<input type="checkbox" v-model="configForm.tools.trivy.enabled">
93+
Enable Trivy
94+
</label>
95+
<div v-if="configForm.tools.trivy.enabled" class="tool-options">
96+
<label for="trivy-scan-type">Trivy Scan Type:</label>
97+
<select id="trivy-scan-type" v-model="configForm.tools.trivy.scanType">
98+
<option value="fs">Filesystem</option>
99+
<option value="image">Image</option>
100+
<option value="repo">Repository</option>
101+
<option value="vuln">Vulnerability Database</option> <!-- Selten direkt hier konfiguriert -->
102+
</select>
103+
<label>Severity Levels (kommagetrennt):</label>
104+
<input type="text" v-model="configForm.tools.trivy.severity" placeholder="UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL">
105+
<small>Standard: HIGH,CRITICAL</small>
106+
<label>
107+
<input type="checkbox" v-model="configForm.tools.trivy.ignoreUnfixed">
108+
Ignore Unfixed Vulnerabilities
109+
</label>
110+
</div>
111+
</div>
112+
113+
<!-- ZAP (Placeholder for now) -->
114+
<div class="tool-config-group">
115+
<label class="tool-enable-label">
116+
<input type="checkbox" v-model="configForm.tools.zap.enabled" disabled>
117+
Enable ZAP (Konfiguration folgt)
118+
</label>
119+
</div>
120+
121+
</fieldset>
122+
<!-- End of New Tool Configuration Section -->
123+
124+
<div v-if="formErrorMessage" class="error-message">{{ formErrorMessage }}</div>
77125
<div class="form-actions">
78126
<button type="submit" :disabled="isSaving" class="action-button save-button">
79127
{{ isSaving ? 'Saving...' : (editingConfiguration ? 'Update Configuration' : 'Create Configuration') }}
@@ -98,7 +146,22 @@ const initialConfigFormState = () => ({
98146
project: null, // Will be set from selectedProjectId prop
99147
has_predefined_targets: false,
100148
target_details_json: '',
101-
tool_configurations_json: ''
149+
tools: {
150+
semgrep: {
151+
enabled: false,
152+
rulesets: 'p/ci', // Standard-Regelsatz als Beispiel
153+
},
154+
trivy: {
155+
enabled: false,
156+
scanType: 'fs', // Standard Scan-Typ
157+
severity: 'HIGH,CRITICAL', // Standard Schweregrade
158+
ignoreUnfixed: false,
159+
},
160+
zap: {
161+
enabled: false,
162+
// Weitere ZAP spezifische einfache Felder hier...
163+
}
164+
}
102165
});
103166
104167
export default {
@@ -197,17 +260,48 @@ export default {
197260
},
198261
prepareEditForm(config) {
199262
this.editingConfiguration = config; // Store the original config object
200-
this.configForm = { // Populate form with its data
263+
264+
let parsedTools = initialConfigFormState().tools; // Start with defaults
265+
if (config.tool_configurations_json) {
266+
try {
267+
const savedToolSettings = JSON.parse(config.tool_configurations_json);
268+
if (savedToolSettings.semgrep) {
269+
parsedTools.semgrep.enabled = savedToolSettings.semgrep.enabled !== undefined ? savedToolSettings.semgrep.enabled : parsedTools.semgrep.enabled;
270+
if (Array.isArray(savedToolSettings.semgrep.rulesets)) {
271+
parsedTools.semgrep.rulesets = savedToolSettings.semgrep.rulesets.join(',');
272+
} else if (typeof savedToolSettings.semgrep.rulesets === 'string') {
273+
parsedTools.semgrep.rulesets = savedToolSettings.semgrep.rulesets;
274+
}
275+
}
276+
if (savedToolSettings.trivy) {
277+
parsedTools.trivy.enabled = savedToolSettings.trivy.enabled !== undefined ? savedToolSettings.trivy.enabled : parsedTools.trivy.enabled;
278+
parsedTools.trivy.scanType = savedToolSettings.trivy.scan_type || parsedTools.trivy.scanType;
279+
if (Array.isArray(savedToolSettings.trivy.severity)) {
280+
parsedTools.trivy.severity = savedToolSettings.trivy.severity.join(',');
281+
} else if (typeof savedToolSettings.trivy.severity === 'string') {
282+
parsedTools.trivy.severity = savedToolSettings.trivy.severity;
283+
}
284+
parsedTools.trivy.ignoreUnfixed = savedToolSettings.trivy.ignore_unfixed !== undefined ? savedToolSettings.trivy.ignore_unfixed : parsedTools.trivy.ignoreUnfixed;
285+
}
286+
// TODO: ZAP parsing if/when ZAP is added
287+
} catch (e) {
288+
console.error('Error parsing tool_configurations_json for editing:', e);
289+
this.formErrorMessage = 'Could not parse existing tool configurations. Please check and re-save.';
290+
// Beibehaltung der Standardwerte für Tools, wenn Parsen fehlschlägt
291+
}
292+
}
293+
294+
this.configForm = {
201295
id: config.id,
202296
name: config.name,
203297
description: config.description || '',
204298
project: config.project,
205299
has_predefined_targets: config.has_predefined_targets || false,
206300
target_details_json: config.target_details_json || '',
207-
tool_configurations_json: config.tool_configurations_json || ''
301+
tools: parsedTools
208302
};
209-
this.showCreateForm = false; // Ensure create mode is off if edit is clicked
210-
this.formErrorMessage = null;
303+
this.showCreateForm = false;
304+
this.formErrorMessage = this.formErrorMessage || null; // Behalte den Parse-Fehler, falls vorhanden
211305
},
212306
async deleteConfiguration(configId) {
213307
if (!confirm(`Are you sure you want to delete configuration ID ${configId}? This cannot be undone.`)) {
@@ -236,7 +330,7 @@ export default {
236330
async saveConfiguration() {
237331
this.isSaving = true;
238332
this.formErrorMessage = null;
239-
// Validate JSON fields before sending if they are not empty
333+
240334
let targetDetailsPayload = null;
241335
if (this.configForm.has_predefined_targets && this.configForm.target_details_json.trim()) {
242336
try {
@@ -252,42 +346,65 @@ export default {
252346
return;
253347
}
254348
255-
let toolConfigsPayload = null;
256-
if (this.configForm.tool_configurations_json.trim()) {
257-
try {
258-
toolConfigsPayload = JSON.parse(this.configForm.tool_configurations_json);
259-
} catch (e) {
260-
this.formErrorMessage = "Tool Configurations JSON is invalid.";
261-
this.isSaving = false;
262-
return;
349+
// Build tool_configurations_json from form data
350+
const toolConfigurations = {};
351+
if (this.configForm.tools.semgrep.enabled) {
352+
toolConfigurations.semgrep = {
353+
enabled: true,
354+
rulesets: this.configForm.tools.semgrep.rulesets.split(',').map(s => s.trim()).filter(s => s)
355+
};
356+
if (toolConfigurations.semgrep.rulesets.length === 0) {
357+
// Backend erwartet vielleicht einen Default oder spezifischen Wert wenn enabled
358+
// Für jetzt, wenn leer, senden wir leeres Array, Backend muss damit umgehen oder wir definieren Default hier
359+
}
360+
}
361+
362+
if (this.configForm.tools.trivy.enabled) {
363+
toolConfigurations.trivy = {
364+
enabled: true,
365+
scan_type: this.configForm.tools.trivy.scanType,
366+
severity: this.configForm.tools.trivy.severity.split(',').map(s => s.trim()).filter(s => s),
367+
ignore_unfixed: this.configForm.tools.trivy.ignoreUnfixed
368+
};
369+
if (toolConfigurations.trivy.severity.length === 0) {
370+
// Ähnlich wie bei Semgrep, Backend-Verhalten oder Default hier definieren
263371
}
264372
}
265373
374+
// TODO: ZAP configuration building
375+
266376
const payload = {
267-
...this.configForm,
268-
target_details_json: targetDetailsPayload, // Send parsed JSON or null
269-
tool_configurations_json: toolConfigsPayload, // Send parsed JSON or null
270-
project: this.configForm.project || this.selectedProjectId // Ensure project is set
377+
name: this.configForm.name,
378+
description: this.configForm.description,
379+
project: this.configForm.project,
380+
has_predefined_targets: this.configForm.has_predefined_targets,
381+
target_info_json: targetDetailsPayload, // Ensure this key matches backend serializer expectations
382+
tool_configurations_json: Object.keys(toolConfigurations).length > 0 ? JSON.stringify(toolConfigurations) : null
271383
};
272384
273-
console.log('ScanConfigurationManager.vue: Attempting to save configuration.');
274-
console.log('ScanConfigurationManager.vue: API URL:', API_CONFIGURATIONS_URL);
275-
console.log('ScanConfigurationManager.vue: Payload:', JSON.parse(JSON.stringify(payload)));
276-
277385
try {
278-
if (this.editingConfiguration) { // Update (PUT)
279-
console.log('ScanConfigurationManager.vue: Sending PUT request to:', `${API_CONFIGURATIONS_URL}${this.editingConfiguration.id}/`);
280-
await axios.put(`${API_CONFIGURATIONS_URL}${this.editingConfiguration.id}/`, payload);
281-
} else { // Create (POST)
282-
console.log('ScanConfigurationManager.vue: Sending POST request to:', API_CONFIGURATIONS_URL);
283-
await axios.post(API_CONFIGURATIONS_URL, payload);
386+
let response;
387+
if (this.editingConfiguration && this.editingConfiguration.id) {
388+
response = await axios.put(`${API_CONFIGURATIONS_URL}${this.editingConfiguration.id}/`, payload);
389+
} else {
390+
response = await axios.post(API_CONFIGURATIONS_URL, payload);
284391
}
285-
await this.fetchConfigurations(this.selectedProjectId); // Refresh list
286-
this.cancelEditOrCreate(); // Close form and reset
392+
// Successfully saved, update local list or re-fetch for the project
393+
this.fetchConfigurations(this.configForm.project);
394+
this.cancelEditOrCreate();
287395
} catch (error) {
288-
console.error('Error saving configuration:', error);
289-
this.formErrorMessage = `Failed to save configuration. ${error.response?.data?.detail || JSON.stringify(error.response?.data) || error.message}`;
290-
if (error.response && error.response.status === 401) {
396+
console.error('Error saving configuration:', error.response || error.message || error);
397+
if (error.response && error.response.data) {
398+
let messages = [];
399+
for (const key in error.response.data) {
400+
const fieldErrors = Array.isArray(error.response.data[key]) ? error.response.data[key].join(', ') : error.response.data[key];
401+
messages.push(`${key.charAt(0).toUpperCase() + key.slice(1)}: ${fieldErrors}`);
402+
}
403+
this.formErrorMessage = messages.join('; ');
404+
} else {
405+
this.formErrorMessage = 'An unknown error occurred while saving the configuration.';
406+
}
407+
if (error.response && error.response.status === 401) {
291408
this.$emit('session-expired');
292409
}
293410
} finally {
@@ -298,6 +415,10 @@ export default {
298415
this.showCreateForm = false;
299416
this.editingConfiguration = null;
300417
this.configForm = initialConfigFormState();
418+
// Wenn selectedProjectId beim Cancel immer noch gesetzt ist, soll es im Formular bleiben für den nächsten "Create New"
419+
if(this.selectedProjectId) {
420+
this.configForm.project = this.selectedProjectId;
421+
}
301422
this.formErrorMessage = null;
302423
}
303424
},
@@ -425,7 +546,8 @@ export default {
425546
font-weight: bold;
426547
}
427548
.form-group input[type="text"],
428-
.form-group textarea {
549+
.form-group textarea,
550+
.form-group select {
429551
width: 100%;
430552
padding: 10px;
431553
border: 1px solid #ced4da;
@@ -446,4 +568,58 @@ export default {
446568
margin-top: 20px;
447569
text-align: right;
448570
}
571+
572+
.tool-config-fieldset {
573+
border: 1px solid #ddd;
574+
padding: 15px;
575+
margin-bottom: 15px;
576+
border-radius: 4px;
577+
}
578+
579+
.tool-config-fieldset legend {
580+
font-weight: bold;
581+
padding: 0 10px;
582+
width: auto; /* Behaves more like a natural legend */
583+
font-size: 1.1em;
584+
}
585+
586+
.tool-config-group {
587+
padding: 10px;
588+
margin-bottom: 10px;
589+
border: 1px dashed #eee;
590+
border-radius: 3px;
591+
}
592+
593+
.tool-enable-label {
594+
font-weight: normal;
595+
display: flex;
596+
align-items: center;
597+
}
598+
599+
.tool-enable-label input[type="checkbox"] {
600+
margin-right: 8px;
601+
width: auto; /* Override global input width for checkbox */
602+
}
603+
604+
.tool-options {
605+
margin-top: 10px;
606+
padding-left: 25px; /* Indent options */
607+
}
608+
609+
.tool-options label {
610+
font-weight: normal;
611+
font-size: 0.95em;
612+
}
613+
614+
.tool-options input[type="text"],
615+
.tool-options select {
616+
margin-bottom: 8px;
617+
}
618+
619+
.tool-options small {
620+
display: block;
621+
font-size: 0.85em;
622+
color: #666;
623+
margin-bottom: 5px;
624+
}
449625
</style>

0 commit comments

Comments
 (0)