blob: 241cbb626a419ac8bd922c5b6d43527f29b361be [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.cloud.oracle.adm;
import com.oracle.bmc.adm.ApplicationDependencyManagementClient;
import com.oracle.bmc.adm.model.ApplicationDependency;
import com.oracle.bmc.adm.model.ApplicationDependencyVulnerabilitySummary;
import com.oracle.bmc.adm.model.CreateVulnerabilityAuditDetails;
import com.oracle.bmc.adm.model.SortOrder;
import com.oracle.bmc.adm.model.Vulnerability;
import com.oracle.bmc.adm.model.VulnerabilityAudit;
import com.oracle.bmc.adm.model.VulnerabilityAudit.LifecycleState;
import com.oracle.bmc.adm.model.VulnerabilityAuditConfiguration;
import com.oracle.bmc.adm.model.VulnerabilityAuditSummary;
import com.oracle.bmc.adm.requests.CreateVulnerabilityAuditRequest;
import com.oracle.bmc.adm.requests.ListApplicationDependencyVulnerabilitiesRequest;
import com.oracle.bmc.adm.requests.ListVulnerabilityAuditsRequest;
import com.oracle.bmc.adm.responses.CreateVulnerabilityAuditResponse;
import com.oracle.bmc.adm.responses.ListApplicationDependencyVulnerabilitiesResponse;
import com.oracle.bmc.adm.responses.ListVulnerabilityAuditsResponse;
import com.oracle.bmc.model.BmcException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.editor.mimelookup.MimeRegistrations;
import org.netbeans.api.lsp.Diagnostic;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.modules.cloud.oracle.OCIManager;
import org.netbeans.modules.project.dependency.ArtifactSpec;
import org.netbeans.modules.project.dependency.Dependency;
import org.netbeans.modules.project.dependency.DependencyResult;
import org.netbeans.modules.project.dependency.ProjectDependencies;
import org.netbeans.modules.project.dependency.Scopes;
import org.netbeans.modules.project.dependency.SourceLocation;
import org.netbeans.spi.lsp.ErrorProvider;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
/**
*
* @author Petr Pisl
*/
@NbBundle.Messages({
"# {0} - project name",
"MSG_Audit_Pass=Vulnerability audit for project {0} is done.\nNo vulnerability was found.",
"# {0} - project name",
"MSG_Audit_Failed_One=Vulnerability audit for project {0} is done.\nOne vulnerability was found.\nThe vulnerability is listed in Problems window.",
"# {0} - project name",
"# {1} - number of vulnerabilities",
"MSG_Audit_Failed_More=Vulnerability audit for project {0} is done.\n{1} vulnerabilities were found.\nThe vulnerabilities are listed in Problems window.",
"# {0} - project name",
"MSG_CreatingAuditFailed=Creating Vulnerablity audit for project {0} failed.",
"# {0} - project name",
"MSG_ListingAuditFailed=Obtaining newly created vulnerablity audit for project {0} failed.",
"MSG_ListingVulnerabilitiesFailed=Obtaining vulnerabilities for newly created audit failed.",
"# {0} - project name",
"MSG_AuditIsRunning=Audit of {0} project is running ...",
"MSG_NotAvailable=Not available",
"MSG_Diagnostic=Vulnerability\n"
+ " Cvss V2 Score: %s\n"
+ " Cvss V3 Score: %s\n"
+ " Caused by dependence: %s"
})
@MimeRegistrations({
@MimeRegistration(mimeType = "text/x-maven-pom+xml", service = ErrorProvider.class),
@MimeRegistration(mimeType = "text/x-gradle+x-groovy", service = ErrorProvider.class)
})
public class VulnerabilityWorker implements ErrorProvider{
private static final RequestProcessor SOURCE_REFRESH_PROCESSOR = new RequestProcessor(VulnerabilityWorker.class.getName());
private static final Logger LOG = Logger.getLogger(VulnerabilityWorker.class.getName());
// PENDING: should be customizable from project configuration somehow.
private static final String GOV_DETAIL_URL = "https://nvd.nist.gov/vuln/detail/";
// @GuardedBy(self)
private static final HashMap<Project, CacheItem> cache = new HashMap();
// @GuardedBy(class)
private static VulnerabilityWorker instance;
/**
* Cached information + watcher over the project file data. Will watch for dependency change event,
* that is fired e.g. after project reload, and will REPLACE ITSELF in the cache + fire
* event that causes LSP to re-evaluate errors for the affected project file(s).
*/
static class CacheItem {
private final Project project;
private final DependencyResult dependencyResult;
private final VulnerabilityReport report;
/**
* Maps GAV -> dependency.
*/
private Map<String, Dependency> dependencyMap;
// @GuardedBy(this)
private ChangeListener depChange;
// @GuardedBy(this)
private RequestProcessor.Task pendingRefresh;
public CacheItem(Project project, DependencyResult dependency, VulnerabilityReport report) {
this.project = project;
this.dependencyResult = dependency;
this.report = report;
}
public DependencyResult getDependency() {
return dependencyResult;
}
public VulnerabilityAuditSummary getAudit() {
return report.summary;
}
public List<ApplicationDependencyVulnerabilitySummary> getVulnerabilities() {
return report.items;
}
public Map<String, Dependency> getDependencyMap() {
if (dependencyMap == null) {
dependencyMap = new HashMap();
buildDependecyMap(dependencyResult.getRoot(), dependencyMap);
}
return dependencyMap;
}
private void buildDependecyMap(Dependency dependency, Map<String, Dependency> result) {
String gav = createGAV(dependency.getArtifact());
if (result.putIfAbsent(gav, dependency) == null) {
dependency.getChildren().forEach((childDependency) -> {
buildDependecyMap(childDependency, result);
});
}
}
public Set<FileObject> getProblematicFiles() {
if (getAudit().getIsSuccess()) {
return Collections.EMPTY_SET;
}
Set<FileObject> result = new HashSet();
for (ApplicationDependencyVulnerabilitySummary v: getVulnerabilities()){
List<Vulnerability> vulnerabilities = v.getVulnerabilities();
if (!vulnerabilities.isEmpty()) {
Dependency dep = getDependencyMap().get(v.getGav());
if (dep != null) {
try {
SourceLocation declarationRange = this.dependencyResult.getDeclarationRange(dep, null);
if (declarationRange == null) {
declarationRange = this.dependencyResult.getDeclarationRange(dep, DependencyResult.PART_CONTAINER);
}
if(declarationRange != null && declarationRange.getFile() != null) {
result.add(declarationRange.getFile());
}
} catch (IOException ex) {
// expected, ignore.
}
}
}
}
return result;
}
void refreshDependencies(RequestProcessor.Task t) {
DependencyResult dr = ProjectDependencies.findDependencies(project, ProjectDependencies.newQuery(Scopes.RUNTIME));
LOG.log(Level.FINER, "{0} - dependencies refreshed", this);
synchronized (this) {
if (pendingRefresh != t) {
return;
}
}
synchronized (this) {
if (depChange != null) {
dependencyResult.removeChangeListener(depChange);
}
}
CacheItem novy = new CacheItem( project, dr, report);
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER, "{0} - trying to replace for {1}", new Object[] { this, novy });
}
if (replaceCacheItem(this, novy)) {
novy.startListening();
Diagnostic.ReporterControl reporter = Diagnostic.findReporterControl(Lookup.getDefault(), project.getProjectDirectory());
Set<FileObject> allFiles = new HashSet<>();
allFiles.addAll(getProblematicFiles());
allFiles.addAll(novy.getProblematicFiles());
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER, "{0} - refreshing files: {1}", new Object[] { this, allFiles });
}
reporter.diagnosticChanged(novy.getProblematicFiles(), null);
}
}
void scheduleDependencyRefresh(ChangeEvent e) {
synchronized (this) {
if (pendingRefresh != null) {
pendingRefresh.cancel();
}
RequestProcessor.Task[] task = new RequestProcessor.Task[1];
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER, "{0} - scheduling refresh for {1}", new Object[] { this, project });
}
pendingRefresh = task[0] = SOURCE_REFRESH_PROCESSOR.post(() -> {
synchronized (this) {
refreshDependencies(task[0]);
}
});
}
}
SourceLocation getDependencyRange(Dependency d) throws IOException {
return getDependencyRange(d, null);
}
void startListening() {
synchronized (this) {
if (depChange == null) {
dependencyResult.addChangeListener(depChange = this::scheduleDependencyRefresh);
LOG.log(Level.FINER, "{0} - start listen for dependencies", this);
}
}
}
SourceLocation getDependencyRange(Dependency d, String part) throws IOException {
startListening();
return dependencyResult.getDeclarationRange(d, part);
}
public List<Diagnostic> getDiagnosticsForFile(FileObject file) {
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER, "{0} getDiagnostics called for {1}", new Object[] { this, file });
}
if (getVulnerabilities() == null || getVulnerabilities().isEmpty()) {
return null;
}
List<Diagnostic> result = new ArrayList();
for (ApplicationDependencyVulnerabilitySummary v: getVulnerabilities()){
List<Vulnerability> vulnerabilities = v.getVulnerabilities();
if (!vulnerabilities.isEmpty()) {
startListening();
Dependency dependency = getDependencyMap().get(v.getGav());
SourceLocation declarationRange = null;
if (dependency != null) {
try {
declarationRange = getDependencyRange(dependency);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
// display the vulnerabilities that were never mapped on the dependency container's line.
// also display the vulnerabilities that we KNOW about, but do can not map them back to the project file, i.e.
// plugin-introduced dependencies (gradle).
if (declarationRange == null && (dependency != null || !report.mappedVulnerabilities.contains(v.getGav()))) {
try {
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER, "{0} getDiagnostics called for {1}", new Object[] { this, file });
}
declarationRange = getDependencyRange(dependency, DependencyResult.PART_CONTAINER);
if (declarationRange != null) {
// discard end location, since it could span a whole section
declarationRange = new SourceLocation(declarationRange.getFile(),
declarationRange.getStartOffset(), declarationRange.getStartOffset(), null);
}
} catch (IOException ex) {
// ignore
}
}
if (declarationRange != null && declarationRange.hasPosition() && declarationRange.getFile().equals(file)) {
final SourceLocation fDeclarationRange = declarationRange;
for(Vulnerability vulnerability: vulnerabilities) {
String message = String.format(Bundle.MSG_Diagnostic(),
formatCvssScore(vulnerability.getCvssV2Score()),
formatCvssScore(vulnerability.getCvssV3Score()),
createGAV(dependency.getArtifact()));
Diagnostic.Builder builder = Diagnostic.Builder.create(() -> fDeclarationRange.getStartOffset(), () -> fDeclarationRange.getEndOffset(), message);
builder.setSeverity(Diagnostic.Severity.Warning).setCode(vulnerability.getId());
try {
builder.setCodeDescription(new URL(GOV_DETAIL_URL + vulnerability.getId()));
} catch (MalformedURLException ex) {
// perhaps should not happen at all
LOG.log(Level.INFO, "Could not link to vulnerability: {0}", vulnerability.getId());
}
result.add(builder.build());
}
}
}
}
return result;
}
private String formatCvssScore(Float value) {
if (value != null) {
return String.format("%.2f", value);
}
return Bundle.MSG_NotAvailable();
}
}
private static boolean replaceCacheItem(CacheItem old, CacheItem novy) {
synchronized (cache) {
CacheItem registered = cache.get(old.project);
if (registered != old) {
return false;
}
cache.put(novy.project, novy);
}
return true;
}
private VulnerabilityWorker() {
}
public static VulnerabilityWorker getInstance() {
synchronized (VulnerabilityWorker.class) {
if (instance == null) {
instance = new VulnerabilityWorker();
}
}
return instance;
}
public void findVulnerability(Project project, boolean forceAudit) {
LOG.log(Level.FINER, "Trying to obtain audit for project {0}, force:{1}", new Object[] { project, forceAudit });
final String projectDisplayName = ProjectUtils.getInformation(project).getDisplayName();
KnowledgeBaseItem kbItem = getKnowledgeBaseForProject(project);
if (kbItem == null) {
return;
}
// remove from the cache old values
ProgressHandle progressHandle = ProgressHandle.createHandle(Bundle.MSG_AuditIsRunning(projectDisplayName));
progressHandle.start();
try {
doFindVulnerability(project, kbItem.compartmentId, kbItem.getKey().getValue(),
projectDisplayName, progressHandle, forceAudit);
} finally {
progressHandle.finish();
kbItem.refresh();
}
}
public void doFindVulnerability(Project project, String compartmentId, String knowledgeBaseId, String projectDisplayName,
ProgressHandle progressHandle, boolean forceAudit) {
DependencyResult dr = ProjectDependencies.findDependencies(project, ProjectDependencies.newQuery(Scopes.RUNTIME));
List<ApplicationDependency> result = new ArrayList();
convert(dr.getRoot(), new HashMap<>(), result);
CacheItem cacheItem = null;
VulnerabilityReport savedAudit = null;
if (!forceAudit) {
try {
savedAudit = AuditCache.getInstance().loadAudit(knowledgeBaseId);
} catch (IOException ex) {
LOG.log(Level.WARNING, "Could not load cached audit data", ex);
}
}
boolean auditDone = false;
if (!forceAudit && savedAudit == null) {
// attempt to find an active most recent audit:
try (ApplicationDependencyManagementClient admClient
= new ApplicationDependencyManagementClient(OCIManager.getDefault().getConfigProvider())) {
ListVulnerabilityAuditsRequest request = ListVulnerabilityAuditsRequest
.builder()
.compartmentId(compartmentId)
.knowledgeBaseId(knowledgeBaseId)
.lifecycleState(LifecycleState.Active)
.sortBy(ListVulnerabilityAuditsRequest.SortBy.TimeCreated)
.sortOrder(SortOrder.Desc).build();
ListVulnerabilityAuditsResponse response = admClient.listVulnerabilityAudits(request);
if (!response.getVulnerabilityAuditCollection().getItems().isEmpty()) {
VulnerabilityAuditSummary summary = response.getVulnerabilityAuditCollection().getItems().get(0);
cacheItem = fetchVulnerabilityItems(project, admClient, dr, summary, projectDisplayName);
savedAudit = new VulnerabilityReport(cacheItem.getAudit(), cacheItem.getVulnerabilities());
}
} catch (BmcException ex) {
LOG.log(Level.FINE, "Unable to list newest audit for knowledgebase {0}, compartment {1}", new Object[] {
knowledgeBaseId, compartmentId
});
}
}
if (savedAudit == null && forceAudit) {
try (ApplicationDependencyManagementClient admClient
= new ApplicationDependencyManagementClient(OCIManager.getDefault().getConfigProvider())) {
final VulnerabilityAuditConfiguration auditConfiguration = VulnerabilityAuditConfiguration
.builder()
.maxPermissibleCvssV2Score(1f)
.maxPermissibleCvssV3Score(1f)
.exclusions(Collections.unmodifiableList(Collections.EMPTY_LIST))
.build();
final CreateVulnerabilityAuditDetails auditDetails = CreateVulnerabilityAuditDetails
.builder()
.compartmentId(compartmentId)
.knowledgeBaseId(knowledgeBaseId)
.displayName(projectDisplayName.replaceAll("[^A-Za-z0-9-_]", "_")) // remove offending characters
.buildType(VulnerabilityAudit.BuildType.Maven)
.configuration(auditConfiguration)
.applicationDependencies(result)
.build();
CreateVulnerabilityAuditResponse response = admClient.createVulnerabilityAudit(CreateVulnerabilityAuditRequest
.builder()
.createVulnerabilityAuditDetails(auditDetails)
.build());
if (response.get__httpStatusCode__() != 201 && response.get__httpStatusCode__() != 200) {
ErrorUtils.processError(response, Bundle.MSG_CreatingAuditFailed(projectDisplayName));
return;
}
// audit is ok
cacheItem = waitToAuditFinish(project, admClient, dr, response.getVulnerabilityAudit(), projectDisplayName);
auditDone = true;
} catch (BmcException exc) {
ErrorUtils.processError(exc, compartmentId);
return;
} finally {
progressHandle.finish();
}
} else if (savedAudit != null) {
cacheItem = new CacheItem(project, dr, savedAudit);
}
if (cacheItem != null) {
synchronized (cache) {
cache.put(project, cacheItem);
}
Set<FileObject> problematicFiles = new HashSet();
problematicFiles.addAll(cacheItem.getProblematicFiles());
String message;
if (cacheItem.getAudit().getIsSuccess()) {
message = Bundle.MSG_Audit_Pass(projectDisplayName);
problematicFiles.addAll(dr.getDependencyFiles());
} else if(cacheItem.getAudit().getVulnerableArtifactsCount() == 1) {
message = Bundle.MSG_Audit_Failed_One(projectDisplayName);
} else {
message = Bundle.MSG_Audit_Failed_More(projectDisplayName, cacheItem.getAudit().getVulnerableArtifactsCount());
}
if (auditDone) {
DialogDisplayer.getDefault().notifyLater(
new NotifyDescriptor.Message(message));
}
Diagnostic.ReporterControl reporter = Diagnostic.findReporterControl(Lookup.getDefault(), project.getProjectDirectory());
reporter.diagnosticChanged(problematicFiles, null);
}
}
public static KnowledgeBaseItem getKnowledgeBaseForProject(Project project) {
ProjectVulnerability vs = project.getLookup().lookup(ProjectVulnerability.class);
return vs != null ? vs.getProjectKnowledgeBase() : null;
}
private int convert(Dependency dependency, Map<String, Integer> gavIndex, List<ApplicationDependency> result) {
String gav = createGAV(dependency.getArtifact());
Integer n = gavIndex.get(gav);
if (n != null) {
return n;
}
gavIndex.put(gav, n = gavIndex.size() + 1);
ApplicationDependency.Builder builder = ApplicationDependency.builder();
builder.gav(gav);
builder.nodeId(Integer.toString(n));
List<String> childrenNodeIds = new ArrayList(dependency.getChildren().size());
for (Dependency childDependency : dependency.getChildren()) {
int cid = convert(childDependency, gavIndex, result);
childrenNodeIds.add(Integer.toString(cid));
}
builder.applicationDependencyNodeIds(childrenNodeIds);
result.add(builder.build());
return n;
}
private static String createGAV(ArtifactSpec artifact) {
StringBuffer sb = new StringBuffer();
sb.append(artifact.getGroupId()).append(':');
sb.append(artifact.getArtifactId()).append(':');
sb.append(artifact.getVersionSpec());
return sb.toString();
}
private CacheItem waitToAuditFinish(Project project, ApplicationDependencyManagementClient client,
DependencyResult dr, VulnerabilityAudit audit, String projectName) {
ListVulnerabilityAuditsRequest request = ListVulnerabilityAuditsRequest.builder()
.knowledgeBaseId(audit.getKnowledgeBaseId()).id(audit.getId()).build();
VulnerabilityAuditSummary auditSummary;
boolean first = true;
do {
if (first) {
first = false;
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
}
ListVulnerabilityAuditsResponse response = client.listVulnerabilityAudits(request);
if (response.get__httpStatusCode__() != 200) {
ErrorUtils.processError(response, Bundle.MSG_ListingAuditFailed(projectName));
return null;
}
List<VulnerabilityAuditSummary> items = response.getVulnerabilityAuditCollection().getItems();
auditSummary = items.get(0);
} while (auditSummary.getLifecycleState() == VulnerabilityAudit.LifecycleState.Creating);
return fetchVulnerabilityItems(project, client, dr, auditSummary, projectName);
}
private CacheItem fetchVulnerabilityItems(Project project, ApplicationDependencyManagementClient client,
DependencyResult dr, VulnerabilityAuditSummary auditSummary, String projectName) {
List<ApplicationDependencyVulnerabilitySummary> items = new ArrayList<>();
if (auditSummary.getVulnerableArtifactsCount() > 0) {
String nextPage = null;
do {
ListApplicationDependencyVulnerabilitiesRequest.Builder b = ListApplicationDependencyVulnerabilitiesRequest.builder().vulnerabilityAuditId(auditSummary.getId());
if (nextPage != null) {
b.page(nextPage);
}
ListApplicationDependencyVulnerabilitiesRequest advRequest = b.build();
ListApplicationDependencyVulnerabilitiesResponse vulners = client.listApplicationDependencyVulnerabilities(advRequest);
if (vulners.get__httpStatusCode__() == 200) {
vulners.getApplicationDependencyVulnerabilityCollection().getItems().stream().filter(v -> !v.getVulnerabilities().isEmpty())
.forEach(v -> items.add(v));
} else {
ErrorUtils.processError(vulners, Bundle.MSG_ListingVulnerabilitiesFailed());
return null;
}
nextPage = vulners.getOpcNextPage();
} while (nextPage != null);
}
// Make an initial scan for the dependency locations in the Dependency report.
Set<String> mapped = new HashSet<>();
VulnerabilityReport report = new VulnerabilityReport(auditSummary, items);
CacheItem cache = new CacheItem(project, dr, report);
for (ApplicationDependencyVulnerabilitySummary v : items) {
List<Vulnerability> vulnerabilities = v.getVulnerabilities();
if (!vulnerabilities.isEmpty()) {
Dependency dependency = cache.getDependencyMap().get(v.getGav());
if (dependency != null) {
mapped.add(v.getGav());
}
}
}
report.setMappedVulnerabilities(mapped);
try {
AuditCache.getInstance().cacheAuditResults(report);
} catch (IOException ex) {
LOG.log(Level.WARNING, "Could not cache audit results for knowledgebase {0}, compartment {1}, project {2}", new Object[] {
auditSummary.getKnowledgeBaseId(), auditSummary.getCompartmentId(),
projectName
});
LOG.log(Level.WARNING, "The exception was: ", ex);
}
return cache;
}
@Override
public List<? extends Diagnostic> computeErrors(Context context) {
List<Diagnostic> result = new ArrayList();
Collection<CacheItem> items;
synchronized (cache) {
items = new ArrayList<>(cache.values());
}
for (CacheItem cacheItem : items) {
List<Diagnostic> diagnostics = cacheItem.getDiagnosticsForFile(context.file());
if (diagnostics != null) {
result.addAll(cacheItem.getDiagnosticsForFile(context.file()));
}
}
return result;
}
}