diff --git a/bundles/com.espressif.idf.ui/OSGI-INF/l10n/bundle.properties b/bundles/com.espressif.idf.ui/OSGI-INF/l10n/bundle.properties index d98fdafb4..d2cf44af9 100644 --- a/bundles/com.espressif.idf.ui/OSGI-INF/l10n/bundle.properties +++ b/bundles/com.espressif.idf.ui/OSGI-INF/l10n/bundle.properties @@ -68,4 +68,5 @@ command.name.PartitionTableEditor = ESP-IDF: Partition Table Editor command.label.nsvTableEditor = ESP-IDF: NVS Table Editor command.name.nvsTableEditor = ESP-IDF: NVS Table Editor command.tooltip.nvsTableEditor = NVS Editor can help you to easily edit NVS CSV, generate encrypted and non-encrypted partitions through GUI, without interacting directly with the csv files. -build_hints.name = Build Hints \ No newline at end of file +build_hints.name = Build Hints +command.label.SbomCommandLabel = ESP-IDF: SBOM Tool \ No newline at end of file diff --git a/bundles/com.espressif.idf.ui/icons/Software_bill_of_materials_(SBOM).png b/bundles/com.espressif.idf.ui/icons/Software_bill_of_materials_(SBOM).png new file mode 100644 index 000000000..a46564888 Binary files /dev/null and b/bundles/com.espressif.idf.ui/icons/Software_bill_of_materials_(SBOM).png differ diff --git a/bundles/com.espressif.idf.ui/plugin.xml b/bundles/com.espressif.idf.ui/plugin.xml index a288f248c..fadc285bb 100644 --- a/bundles/com.espressif.idf.ui/plugin.xml +++ b/bundles/com.espressif.idf.ui/plugin.xml @@ -428,6 +428,22 @@ + + + + + + + + + + diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/dialogs/SbomCommandDialog.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/dialogs/SbomCommandDialog.java new file mode 100644 index 000000000..677fa0393 --- /dev/null +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/dialogs/SbomCommandDialog.java @@ -0,0 +1,455 @@ +package com.espressif.idf.ui.dialogs; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.console.IHyperlink; +import org.eclipse.ui.console.IPatternMatchListener; +import org.eclipse.ui.console.MessageConsoleStream; +import org.eclipse.ui.console.PatternMatchEvent; +import org.eclipse.ui.console.TextConsole; +import org.eclipse.ui.ide.IDE; + +import com.espressif.idf.core.IDFCorePlugin; +import com.espressif.idf.core.ProcessBuilderFactory; +import com.espressif.idf.core.logging.Logger; +import com.espressif.idf.core.util.IDFUtil; +import com.espressif.idf.core.util.StringUtil; +import com.espressif.idf.ui.IDFConsole; +import com.espressif.idf.ui.update.Messages; + +public class SbomCommandDialog extends TitleAreaDialog +{ + + private static final String PATH_REGEX = "(.+)"; //$NON-NLS-1$ + private static final String DEFAULT_OUTPUT_FILE_NAME = "sbom.txt"; //$NON-NLS-1$ + private static final String ESP_IDF_SBOM_COMMAND_NAME = "esp_idf_sbom"; //$NON-NLS-1$ + protected static final String[] EXTENSIONS = { "*.json" }; //$NON-NLS-1$ + private MessageConsoleStream console; + private Button saveOutputToFileCheckBoxButton; + private Text outputFileText; + private IProject selectedProject; + private Text projectDescriptionPathText; + private String projectDescription; + private boolean saveOutputFileStatus; + private String outputFilePath; + + public SbomCommandDialog(Shell parentShell) + { + super(parentShell); + } + + @Override + public void create() + { + super.create(); + getShell().setText(Messages.SbomCommandDialog_SbomTitle); + setTitle(Messages.SbomCommandDialog_SbomTitle); + setMessage(Messages.SbomCommandDialog_SbomInfoMsg); + setDefaults(); + } + + @Override + protected Control createDialogArea(Composite parent) + { + Composite area = (Composite) super.createDialogArea(parent); + + Composite container = new Composite(area, SWT.NONE); + container.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + GridLayout layout = new GridLayout(3, false); + container.setLayout(layout); + + saveOutputToFileCheckBoxButton = new Button(container, SWT.CHECK); + saveOutputToFileCheckBoxButton.setText(Messages.SbomCommandDialog_RedirectOutputCheckBoxLbl); + saveOutputToFileCheckBoxButton.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1)); + + Label projectDescriptionPathLbl = new Label(container, SWT.NONE); + projectDescriptionPathLbl.setText(Messages.SbomCommandDialog_ProjectDescriptionPathLbl); + projectDescriptionPathText = new Text(container, SWT.SINGLE | SWT.BORDER | SWT.H_SCROLL); + projectDescriptionPathText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + Button projectDescriptionBtn = new Button(container, SWT.PUSH); + projectDescriptionBtn.setLayoutData(new GridData(SWT.RIGHT, SWT.FILL, false, false, 1, 1)); + projectDescriptionBtn.setText(Messages.SbomCommandDialog_BrowseBtnTxt); + projectDescriptionBtn.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) + { + FileDialog fileSelectionDialog = new FileDialog(getParentShell()); + fileSelectionDialog.setFilterExtensions(EXTENSIONS); + fileSelectionDialog.setFilterPath(buildProjectDescriptionPath()); + String selectedFilePath = fileSelectionDialog.open(); + + if (selectedFilePath != null && !selectedFilePath.isEmpty()) + { + projectDescriptionPathText.setText(selectedFilePath); + } + super.widgetSelected(e); + } + }); + + Label outputFileLbl = new Label(container, SWT.NONE); + outputFileLbl.setText(Messages.SbomCommandDialog_OutputFilePathLbl); + outputFileText = new Text(container, SWT.NONE); + outputFileText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + Button outputFileBrowseBtn = new Button(container, SWT.PUSH); + outputFileBrowseBtn.setText(Messages.SbomCommandDialog_BrowseBtnTxt); + outputFileBrowseBtn.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) + { + FileDialog fileSelectionDialog = new FileDialog(getParentShell()); + fileSelectionDialog.setFilterPath(buildProjectDescriptionPath()); + String selectedFilePath = fileSelectionDialog.open(); + + if (selectedFilePath != null && !selectedFilePath.isEmpty()) + { + outputFileText.setText(selectedFilePath); + } + super.widgetSelected(e); + } + }); + saveOutputToFileCheckBoxButton.addListener(SWT.Selection, e -> { + outputFileLbl.setVisible(saveOutputToFileCheckBoxButton.getSelection()); + outputFileText.setVisible(saveOutputToFileCheckBoxButton.getSelection()); + outputFileBrowseBtn.setVisible(saveOutputToFileCheckBoxButton.getSelection()); + container.requestLayout(); + }); + + return super.createDialogArea(parent); + } + + protected String runCommand(List arguments, Path workDir, Map env) + { + String exportCmdOp = StringUtil.EMPTY; + ProcessBuilderFactory processRunner = new ProcessBuilderFactory(); + try + { + IStatus status = processRunner.runInBackground(arguments, workDir, env); + if (status == null) + { + IStatus errorStatus = IDFCorePlugin.errorStatus(Messages.SbomCommandDialog_StatusCantBeNullErrorMsg, + null); + Logger.log(IDFCorePlugin.getPlugin(), errorStatus); + return errorStatus.getMessage(); + } + + exportCmdOp = status.getMessage(); + Logger.log(exportCmdOp); + } + catch (IOException e) + { + Logger.log(e); + } + return exportCmdOp; + } + + @Override + protected void okPressed() + { + console = new IDFConsole().getConsoleStream(Messages.IDFToolsHandler_ToolsManagerConsole, null, false); + Job refreshJob = new Job(Messages.SbomCommandDialog_RefreshProjectJob) + { + + protected IStatus run(IProgressMonitor monitor) + { + try + { + selectedProject.refreshLocal(IResource.DEPTH_INFINITE, null); + } + catch (CoreException e) + { + Logger.log(e); + } + return Status.OK_STATUS; + } + }; + Job espIdfSbomJob = new Job(Messages.SbomCommandDialog_EspIdfSbomJobName) + { + + protected IStatus run(IProgressMonitor monitor) + { + if (!getEspIdfSbomInstalledStatus()) + { + installEspIdfSbom(); + } + runEspIdfSbomCommand(); + try + { + refreshJob.join(); + } + catch (InterruptedException e) + { + Logger.log(e); + } + return Status.OK_STATUS; + } + }; + + projectDescription = projectDescriptionPathText.getText(); + saveOutputFileStatus = saveOutputToFileCheckBoxButton.getSelection(); + outputFilePath = outputFileText.getText(); + espIdfSbomJob.schedule(); + + super.okPressed(); + } + + @Override + protected boolean isResizable() + { + return true; + } + + private void setDefaults() + { + ISelection selection = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getSelectionService() + .getSelection(); + if (selection instanceof IStructuredSelection) + { + Object element = ((IStructuredSelection) selection).getFirstElement(); + + if (element instanceof IResource) + { + selectedProject = ((IResource) element).getProject(); + } + } + projectDescriptionPathText.setText(buildProjectDescriptionPath()); + if (!Files.isRegularFile(Paths.get(projectDescriptionPathText.getText()))) + { + setMessage(Messages.SbomCommandDialog_ProjectDescDoesntExistDefaultErrorMsg); + getButton(IDialogConstants.OK_ID).setEnabled(false); + } + outputFileText.setText(String.join(FileSystems.getDefault().getSeparator(), + Paths.get(selectedProject.getLocationURI()).toString(), DEFAULT_OUTPUT_FILE_NAME)); + + saveOutputToFileCheckBoxButton.setSelection(false); + saveOutputToFileCheckBoxButton.notifyListeners(SWT.Selection, null); + + outputFileText.addListener(SWT.Modify, e -> getButton(IDialogConstants.OK_ID).setEnabled(validateInput())); + projectDescriptionPathText.addListener(SWT.Modify, + e -> getButton(IDialogConstants.OK_ID).setEnabled(validateInput())); + } + + private String buildProjectDescriptionPath() + { + return String.join(FileSystems.getDefault().getSeparator(), + Paths.get(selectedProject.getLocationURI()).toString(), "build", //$NON-NLS-1$ + "project_description.json"); //$NON-NLS-1$ + } + + private void runEspIdfSbomCommand() + { + Map environment = new HashMap<>(System.getenv()); + List arguments = new ArrayList<>(); + final String pythonEnvPath = IDFUtil.getIDFPythonEnvPath(); + arguments.add(pythonEnvPath); + arguments.add("-m"); //$NON-NLS-1$ + arguments.add(ESP_IDF_SBOM_COMMAND_NAME); + arguments.add("create"); //$NON-NLS-1$ + arguments.add(projectDescription); + if (saveOutputFileStatus) + { + arguments.add("--output-file"); //$NON-NLS-1$ + arguments.add(outputFilePath); + } + String cmdOutput = runCommand(arguments, null, environment); + cmdOutput = cmdOutput.isEmpty() && saveOutputFileStatus + ? String.format(Messages.SbomCommandDialog_ConsoleRedirectedOutputFormatString, outputFilePath) + : cmdOutput; + console.getConsole().addPatternMatchListener(getPatternMatchListener()); + console.println(cmdOutput); + + } + + private IPatternMatchListener getPatternMatchListener() + { + return new IPatternMatchListener() + { + + public void matchFound(PatternMatchEvent event) + { + + try + { + IHyperlink hepHyperlink = createHyperlinkWhichOpensFileInEditor(); + // calculating the right offset and length to highlight only the output path + console.getConsole().addHyperlink(hepHyperlink, getPattern().length() - PATH_REGEX.length(), + event.getLength() - getPattern().length() + PATH_REGEX.length()); + } + catch (BadLocationException e) + { + Logger.log(e); + } + } + + private IHyperlink createHyperlinkWhichOpensFileInEditor() + { + return new IHyperlink() + { + public void linkActivated() + { + IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(outputFilePath)); + if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) + { + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + try + { + IDE.openEditorOnFileStore(page, fileStore); + } + catch (PartInitException e) + { + Logger.log(e); + } + } + + } + + public void linkEntered() + { + } + + public void linkExited() + { + } + }; + } + + public void disconnect() + { + } + + public void connect(TextConsole console) + { + } + + public String getPattern() + { + return String.format(Messages.SbomCommandDialog_ConsoleRedirectedOutputFormatString, PATH_REGEX); + } + + public String getLineQualifier() + { + return null; + } + + public int getCompilerFlags() + { + return 0; + } + }; + } + + private boolean validateInput() + { + boolean validateStatus = true; + java.nio.file.Path projectDescriptionPath = null; + try + { + projectDescriptionPath = Paths.get(projectDescriptionPathText.getText()); + } + catch (InvalidPathException e) + { + validateStatus = false; + setErrorMessage(Messages.SbomCommandDialog_InvalidProjectDescPathErrorMsg); + } + if (projectDescriptionPath != null && !Files.isRegularFile(projectDescriptionPath)) + { + validateStatus = false; + setErrorMessage(Messages.SbomCommandDialog_ProjectDescDoesntExistsErrorMsg); + } + + java.nio.file.Path outputFilePath = null; + try + { + outputFilePath = Paths.get(outputFileText.getText()); + } + catch (InvalidPathException e) + { + validateStatus = false; + setErrorMessage(Messages.SbomCommandDialog_InvalidOutputFilePathErrorMsg); + } + if (outputFilePath != null && saveOutputToFileCheckBoxButton.getSelection() + && checkIfFileIsNotWritable(outputFilePath)) + { + validateStatus = false; + setErrorMessage(Messages.SbomCommandDialog_OutputFileNotWritabbleErrorMsg); + } + + if (validateStatus) + { + setErrorMessage(null); + } + return validateStatus; + } + + private boolean checkIfFileIsNotWritable(java.nio.file.Path pathToFile) + { + return Files.exists(pathToFile) && !Files.isWritable(pathToFile); + } + + private void installEspIdfSbom() + { + Map environment = new HashMap<>(System.getenv()); + List arguments = new ArrayList<>(); + final String pythonEnvPath = IDFUtil.getIDFPythonEnvPath(); + arguments.add(pythonEnvPath); + arguments.add("-m"); //$NON-NLS-1$ + arguments.add("pip"); //$NON-NLS-1$ + arguments.add("install"); //$NON-NLS-1$ + arguments.add(ESP_IDF_SBOM_COMMAND_NAME); + String cmdOutput = runCommand(arguments, null, environment); + console.println(cmdOutput); + + } + + private boolean getEspIdfSbomInstalledStatus() + { + Map environment = new HashMap<>(System.getenv()); + List arguments = new ArrayList<>(); + final String pythonEnvPath = IDFUtil.getIDFPythonEnvPath(); + arguments.add(pythonEnvPath); + arguments.add("-m"); //$NON-NLS-1$ + arguments.add("pip"); //$NON-NLS-1$ + arguments.add("list"); //$NON-NLS-1$ + String cmdOutput = runCommand(arguments, null, environment); + return cmdOutput.contains("esp-idf-sbom"); //$NON-NLS-1$ + } +} diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/handlers/SbomCommandHandler.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/handlers/SbomCommandHandler.java new file mode 100644 index 000000000..1c5b00826 --- /dev/null +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/handlers/SbomCommandHandler.java @@ -0,0 +1,23 @@ +package com.espressif.idf.ui.handlers; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; + +import com.espressif.idf.ui.dialogs.SbomCommandDialog; + +public class SbomCommandHandler extends AbstractHandler +{ + + public Object execute(ExecutionEvent event) throws ExecutionException + { + Shell activeShell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + SbomCommandDialog flashDialog = new SbomCommandDialog(activeShell); + flashDialog.create(); + flashDialog.open(); + return null; + } + +} diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/update/Messages.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/update/Messages.java index 09f8502c6..ce52527eb 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/update/Messages.java +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/update/Messages.java @@ -51,7 +51,23 @@ public class Messages extends NLS public static String PythonIdfEnvMsg; public static String MissingIdfPathMsg; public static String NotFoundMsg; - + + public static String SbomCommandDialog_BrowseBtnTxt; + public static String SbomCommandDialog_ConsoleRedirectedOutputFormatString; + public static String SbomCommandDialog_EspIdfSbomJobName; + public static String SbomCommandDialog_InvalidProjectDescPathErrorMsg; + public static String SbomCommandDialog_InvalidOutputFilePathErrorMsg; + public static String SbomCommandDialog_OutputFileNotWritabbleErrorMsg; + public static String SbomCommandDialog_OutputFilePathLbl; + public static String SbomCommandDialog_ProjectDescDoesntExistDefaultErrorMsg; + public static String SbomCommandDialog_ProjectDescDoesntExistsErrorMsg; + public static String SbomCommandDialog_ProjectDescriptionPathLbl; + public static String SbomCommandDialog_RedirectOutputCheckBoxLbl; + public static String SbomCommandDialog_RefreshProjectJob; + public static String SbomCommandDialog_SbomInfoMsg; + public static String SbomCommandDialog_SbomTitle; + public static String SbomCommandDialog_StatusCantBeNullErrorMsg; + static { // initialize resource bundle diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/update/messages.properties b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/update/messages.properties index 94c6ac3de..4ffa881ee 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/update/messages.properties +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/update/messages.properties @@ -44,3 +44,18 @@ EclipseMsg=Eclipse Version: PythonIdfEnvMsg=Python set for IDF_PYTHON_ENV: MissingIdfPathMsg=ESP-IDF version cannot be checked. IDF_PATH or IDF_PYTHON_ENV_PATH are not set. NotFoundMsg= +SbomCommandDialog_BrowseBtnTxt=Browse +SbomCommandDialog_ConsoleRedirectedOutputFormatString=The output was redirected to the file: %s +SbomCommandDialog_EspIdfSbomJobName=Running esp-idf-sbom command... +SbomCommandDialog_InvalidProjectDescPathErrorMsg=The path to project_description.json is invalid +SbomCommandDialog_InvalidOutputFilePathErrorMsg=The path to the output file is invalid +SbomCommandDialog_OutputFileNotWritabbleErrorMsg=The selected output file is not writable +SbomCommandDialog_OutputFilePathLbl=Output File Path: +SbomCommandDialog_ProjectDescDoesntExistDefaultErrorMsg=Provided project_description.json doesn't exist. Build the project first. +SbomCommandDialog_ProjectDescDoesntExistsErrorMsg=Provided project_description.json doesn't exist. +SbomCommandDialog_ProjectDescriptionPathLbl=Project Description Path: +SbomCommandDialog_RedirectOutputCheckBoxLbl=Redirect output to the file +SbomCommandDialog_RefreshProjectJob=Refreshing the project +SbomCommandDialog_SbomInfoMsg=Provide path to the project description file and click OK. The result of the command will be displayed on the console +SbomCommandDialog_SbomTitle=Software Bill of Materials Tool +SbomCommandDialog_StatusCantBeNullErrorMsg=Operation status cannot be null. Please ensure the operation was executed correctly.