|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to configure the CycloneDX SBOM generation. |
|
8 """ |
|
9 |
|
10 import os |
|
11 |
|
12 from PyQt6.QtCore import pyqtSlot |
|
13 from PyQt6.QtWidgets import QDialog, QDialogButtonBox |
|
14 |
|
15 from EricWidgets.EricApplication import ericApp |
|
16 from EricWidgets.EricPathPicker import EricPathPickerModes |
|
17 |
|
18 from .Ui_CycloneDXConfigDialog import Ui_CycloneDXConfigDialog |
|
19 |
|
20 |
|
21 class CycloneDXConfigDialog(QDialog, Ui_CycloneDXConfigDialog): |
|
22 """ |
|
23 Class implementing a dialog to configure the CycloneDX SBOM generation. |
|
24 """ |
|
25 SupportedSchemas = { |
|
26 "JSON": [ |
|
27 (1, 4), |
|
28 (1, 3), |
|
29 (1, 2), |
|
30 ], |
|
31 "XML": [ |
|
32 (1, 4), |
|
33 (1, 3), |
|
34 (1, 2), |
|
35 (1, 1), |
|
36 (1, 0), |
|
37 ], |
|
38 } |
|
39 Sources = { |
|
40 "pipenv": "Pipfile.lock", |
|
41 "poetry": "poetry.lock", |
|
42 "requirements": "requirements.txt", |
|
43 } |
|
44 DefaultFileFormat = "JSON" |
|
45 DefaultFileNames = { |
|
46 "JSON": "cyclonedx.json", |
|
47 "XML": "cyclonedx.xml", |
|
48 } |
|
49 |
|
50 def __init__(self, environment, parent=None): |
|
51 """ |
|
52 Constructor |
|
53 |
|
54 @param environment name of the virtual environment |
|
55 @type str |
|
56 @param parent reference to the parent widget (defaults to None) |
|
57 @type QWidget (optional) |
|
58 """ |
|
59 super().__init__(parent) |
|
60 self.setupUi(self) |
|
61 |
|
62 if environment == "<project>": |
|
63 self.__project = ericApp().getObject("Project") |
|
64 self.__defaultDirectory = self.__project.getProjectPath() |
|
65 else: |
|
66 self.__project = None |
|
67 venvManager = ericApp().getObject("VirtualEnvManager") |
|
68 self.__defaultDirectory = venvManager.getVirtualenvDirectory( |
|
69 environment) |
|
70 |
|
71 self.environmentLabel.setText(environment) |
|
72 |
|
73 self.pipenvButton.setEnabled(os.path.isfile(os.path.join( |
|
74 self.__defaultDirectory, |
|
75 CycloneDXConfigDialog.Sources["pipenv"] |
|
76 ))) |
|
77 self.poetryButton.setEnabled(os.path.isfile(os.path.join( |
|
78 self.__defaultDirectory, |
|
79 CycloneDXConfigDialog.Sources["poetry"] |
|
80 ))) |
|
81 self.requirementsButton.setEnabled(os.path.isfile(os.path.join( |
|
82 self.__defaultDirectory, |
|
83 CycloneDXConfigDialog.Sources["requirements"] |
|
84 ))) |
|
85 |
|
86 self.vulnerabilityCheckBox.toggled.connect( |
|
87 self.__repopulateSchemaVersionComboBox) |
|
88 |
|
89 self.filePicker.setMode(EricPathPickerModes.SAVE_FILE_OVERWRITE_MODE) |
|
90 self.filePicker.setDefaultDirectory(self.__defaultDirectory) |
|
91 |
|
92 self.fileFormatComboBox.setCurrentText( |
|
93 CycloneDXConfigDialog.DefaultFileFormat) |
|
94 self.on_fileFormatComboBox_currentTextChanged( |
|
95 CycloneDXConfigDialog.DefaultFileFormat) |
|
96 |
|
97 self.__metadata = None |
|
98 self.__metadataButton = self.buttonBox.addButton( |
|
99 self.tr("Edit Metadata..."), |
|
100 QDialogButtonBox.ButtonRole.ActionRole) |
|
101 self.__metadataButton.clicked.connect(self.__editMetaData) |
|
102 |
|
103 msh = self.minimumSizeHint() |
|
104 self.resize(max(self.width(), msh.width()), msh.height()) |
|
105 |
|
106 @pyqtSlot() |
|
107 def __repopulateSchemaVersionComboBox(self): |
|
108 """ |
|
109 Private slot to repopulate the schema version selector. |
|
110 """ |
|
111 fileFormat = self.fileFormatComboBox.currentText() |
|
112 minSchemaVersion = ( |
|
113 (1, 4) |
|
114 if self.vulnerabilityCheckBox.isChecked() else |
|
115 (1, 0) |
|
116 ) |
|
117 self.schemaVersionComboBox.clear() |
|
118 self.schemaVersionComboBox.addItems( |
|
119 "{0}.{1}".format(*f) |
|
120 for f in CycloneDXConfigDialog.SupportedSchemas[fileFormat] |
|
121 if f >= minSchemaVersion |
|
122 ) |
|
123 |
|
124 @pyqtSlot(str) |
|
125 def on_fileFormatComboBox_currentTextChanged(self, fileFormat): |
|
126 """ |
|
127 Private slot to handle the selection of a SBOM file format. |
|
128 |
|
129 @param fileFormat selected format |
|
130 @type str |
|
131 """ |
|
132 # re-populate the file schema combo box |
|
133 self.__repopulateSchemaVersionComboBox() |
|
134 |
|
135 # set the file filter |
|
136 if fileFormat == "JSON": |
|
137 self.filePicker.setFilters( |
|
138 self.tr("JSON Files (*.json);;All Files (*)")) |
|
139 elif fileFormat == "XML": |
|
140 self.filePicker.setFilters( |
|
141 self.tr("XML Files (*.xml);;All Files (*)")) |
|
142 else: |
|
143 self.filePicker.setFilters(self.tr("All Files (*)")) |
|
144 |
|
145 @pyqtSlot() |
|
146 def __editMetaData(self): |
|
147 """ |
|
148 Private slot to open a dialog for editing the SBOM metadata. |
|
149 """ |
|
150 from .CycloneDXMetaDataDialog import CycloneDXMetaDataDialog |
|
151 |
|
152 # populate a metadata dictionary from project data |
|
153 metadata = ( |
|
154 { |
|
155 "Name": self.__project.getProjectName(), |
|
156 "Type": "", |
|
157 "Version": self.__project.getProjectVersion(), |
|
158 "Description": self.__project.getProjectDescription(), |
|
159 "AuthorName": self.__project.getProjectAuthor(), |
|
160 "AuthorEmail": self.__project.getProjectAuthorEmail(), |
|
161 "License": self.__project.getProjectLicense(), |
|
162 "Manufacturer": "", |
|
163 "Supplier": "", |
|
164 } |
|
165 if self.__metadata is None and self.__project is not None else |
|
166 self.__metadata |
|
167 ) |
|
168 |
|
169 dlg = CycloneDXMetaDataDialog(metadata=metadata, parent=self) |
|
170 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
171 self.__metadata = dlg.getMetaData() |
|
172 |
|
173 def getData(self): |
|
174 """ |
|
175 Public method to get the SBOM configuration data. |
|
176 |
|
177 @return tuple containing the input source, the input file name, the |
|
178 file format, the schema version, the path of the SBOM file to be |
|
179 written, a flag indicating to include vulnerability information, |
|
180 a flag indicating to include dependency information and a |
|
181 dictionary containing the SBOM meta data |
|
182 @rtype tuple of (str, str, str, str, str, bool, bool, dict) |
|
183 """ |
|
184 if self.environmentButton.isChecked(): |
|
185 inputSource = "environment" |
|
186 inputFile = None |
|
187 elif self.pipenvButton.isChecked(): |
|
188 inputSource = "pipenv" |
|
189 inputFile = os.path.join( |
|
190 self.__defaultDirectory, |
|
191 CycloneDXConfigDialog.Sources["pipenv"] |
|
192 ) |
|
193 elif self.poetryButton.isChecked(): |
|
194 inputSource = "poetry" |
|
195 inputFile = os.path.join( |
|
196 self.__defaultDirectory, |
|
197 CycloneDXConfigDialog.Sources["poetry"] |
|
198 ) |
|
199 elif self.requirementsButton.isChecked(): |
|
200 inputSource = "requirements" |
|
201 inputFile = os.path.join( |
|
202 self.__defaultDirectory, |
|
203 CycloneDXConfigDialog.Sources["requirements"] |
|
204 ) |
|
205 else: |
|
206 # should not happen |
|
207 inputSource = None |
|
208 inputFile = None |
|
209 |
|
210 fileFormat = self.fileFormatComboBox.currentText() |
|
211 schemaVersion = self.schemaVersionComboBox.currentText() |
|
212 sbomFile = self.filePicker.text() |
|
213 if not sbomFile: |
|
214 try: |
|
215 sbomFile = os.path.join( |
|
216 self.__defaultDirectory, |
|
217 CycloneDXConfigDialog.DefaultFileNames[fileFormat] |
|
218 ) |
|
219 except KeyError: |
|
220 # should not happen |
|
221 sbomFile = None |
|
222 |
|
223 return ( |
|
224 inputSource, inputFile, fileFormat, schemaVersion, sbomFile, |
|
225 self.vulnerabilityCheckBox.isChecked(), |
|
226 self.dependenciesCheckBox.isChecked(), |
|
227 self.__metadata |
|
228 ) |