Merge pull request #6972 from sdedic/adm/multiple-vulnerability-sources

Report all occurences of a vulnerability.
diff --git a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/adm/VulnerabilityWorker.java b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/adm/VulnerabilityWorker.java
index b00f423..68adc66 100644
--- a/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/adm/VulnerabilityWorker.java
+++ b/enterprise/cloud.oracle/src/org/netbeans/modules/cloud/oracle/adm/VulnerabilityWorker.java
@@ -43,12 +43,14 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import java.util.stream.Collectors;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 import org.netbeans.api.editor.mimelookup.MimeRegistration;
@@ -71,8 +73,8 @@
 import org.openide.util.Exceptions;
 import org.openide.util.Lookup;
 import org.openide.util.NbBundle;
+import org.openide.util.Pair;
 import org.openide.util.RequestProcessor;
-import org.openide.util.RequestProcessor.Task;
 
 /**
  *
@@ -131,6 +133,192 @@
     // @GuardedBy(class)
     private static VulnerabilityWorker instance;
 
+    private static String nonNull(String s) {
+        return s == null ? "" : s;
+    }
+    
+    /**
+     * Holds mapping from vulnerable dependencies to the source. This is invalidated and
+     * recomputed after each source change from the same dependency result.
+     * This is computed for dependencies found in vulnerability items.
+     */
+    final static class SourceMapping {
+        /**
+         * For each reported Depdendency, its closest parent (including self) with source
+         * information.
+         */
+        final Map<Dependency, Dependency> nodesWithSource = new HashMap<>();
+
+        /**
+         * Source locations for the dependencies. Note the SourceLocation may be implied,
+         * so that it points to a different Dependnecy.
+         */
+        final Map<Dependency, SourceLocation> locations = new HashMap<>();
+        
+        /**
+         * Set of files with reported locations. Usually just one.
+         */
+        final Set<FileObject> files = new HashSet<>();
+    }
+
+
+    /**
+     * Records all vulnerabilities for a certain group-artifact-version. Collects all
+     * locations (paths) in the dependency tree where the GAV appears.
+     */
+    final static class VulnerabilityItem {
+        /**
+         * GAV coordinates of the reported artifact
+         */
+        final String gav;
+        /**
+         * List of vulnerabilities found for this artifact GAV
+         */
+        final List<Vulnerability> reports;
+        
+        /**
+         * All paths to the artifact through the dependency graph. Each List is a path
+         * from the project root to the identified dependency.
+         */
+        final Set<List<Dependency>>  paths = new LinkedHashSet<>();
+        
+        public VulnerabilityItem(String gav, List<Vulnerability> reports) {
+            this.gav = gav;
+            this.reports = reports;
+        }
+    }
+    
+    private static String getDependencyId(Dependency d) {
+        if (d.getProject() != null) {
+            return d.getProject().getProjectId();
+        } else if (d.getArtifact() != null) {
+            return createGAV(d.getArtifact());
+        } else {
+            return d.toString();
+        }
+    }
+    
+    static Pair<Dependency, SourceLocation> findSource(DependencyResult dependencyResult, Dependency dependency) {
+        SourceLocation sourcePath = null;
+        Dependency sd = dependency;
+        for (; sd != null; sd = sd.getParent()) {
+            try {
+                sourcePath = dependencyResult.getDeclarationRange(dependency, null);
+                if (sourcePath != null) {
+                    break;
+                }
+            } catch (IOException ex) {
+                LOG.log(Level.WARNING, "Could not load dependency source", ex);
+            }
+        }
+        return Pair.of(sd, sourcePath);
+    }
+    
+    static class SourceMappingBuilder {
+        private final DependencyResult dependencyResult;
+        private final Map<String, VulnerabilityItem> itemIndex;
+        private SourceMapping result = new SourceMapping();
+
+        public SourceMappingBuilder(DependencyResult dependencyResult, Map<String, VulnerabilityItem> itemIndex) {
+            this.dependencyResult = dependencyResult;
+            this.itemIndex = itemIndex;
+        }
+        
+        public SourceMapping build() {
+            for (VulnerabilityItem item : itemIndex.values()) {
+                for (List<Dependency> path : item.paths) {
+                    Dependency node = path.get(path.size() - 1);
+                    if (result.nodesWithSource.containsKey(node)) {
+                        continue;
+                    }
+                    Pair<Dependency, SourceLocation> s = findSource(dependencyResult, node);
+                    result.nodesWithSource.put(node, s.first());
+                    if (s.first() == null || s.second() == null) {
+                        continue;
+                    }
+                    SourceLocation l = s.second();
+                    result.nodesWithSource.put(node, s.first());
+                    if (l != null) {
+                        result.locations.putIfAbsent(s.first(), l);
+                        result.files.add(l.getFile());
+                    }
+                }
+            }
+            return result;
+        }
+    }
+
+    
+    /**
+     * Builds the vulnerability indices from the vulnerability report and project dependencies. The outcome is "itemIdex" and
+     * "sourceMapping" which are then copied to {@link CacheItem}.
+     */
+    static class CacheDataBuilder {
+        private final DependencyResult dependencyResult;
+        private final VulnerabilityReport report;
+        
+        private Map<String, VulnerabilityItem> itemIndex = new HashMap<>();
+        private SourceMapping sourceMapping;
+        private Set<String> uniqueDeps = new HashSet<>();
+
+        public CacheDataBuilder(DependencyResult dependencyResult, VulnerabilityReport report) {
+            this.dependencyResult = dependencyResult;
+            this.report = report;
+        }
+
+        private void initVulnerabilityIndex() {
+            this.itemIndex = new HashMap<>();
+            for (ApplicationDependencyVulnerabilitySummary s: report.items) {
+                VulnerabilityItem item = new VulnerabilityItem(s.getGav(), s.getVulnerabilities());
+                itemIndex.put(s.getGav(), item);
+            }
+        }
+        
+        private void buildDependencyMap(Dependency dependency, List<Dependency> path, Set<String> pathToRoot) {
+            String gav = getDependencyId(dependency);
+            if (gav == null || !pathToRoot.add(gav)) {
+                return;
+            }
+            uniqueDeps.add(gav);
+            
+            pathToRoot.add(gav);
+            try {
+                VulnerabilityItem item = itemIndex.get(gav);
+                if (item != null) {
+                    List<Dependency> p = new ArrayList<>(path);
+                    p.add(dependency);
+                    item.paths.add(p);
+                }
+                
+                if (dependency != dependencyResult.getRoot()) {
+                    path.add(dependency);
+                }
+                dependency.getChildren().forEach((childDependency) -> {
+                    buildDependencyMap(childDependency, path, pathToRoot);
+                });
+                if (!path.isEmpty()) {
+                    Dependency x = path.remove(path.size() - 1);
+                    assert x == dependency;
+                }
+            } finally {
+                pathToRoot.remove(gav);
+            }
+        }
+        
+        Map<String, VulnerabilityItem> build() {
+            initVulnerabilityIndex();
+            SourceLocation rootLocation = null;
+            try {
+                rootLocation = dependencyResult.getDeclarationRange(dependencyResult.getRoot(), null);
+            } catch (IOException ex) {
+                LOG.log(Level.WARNING, "Could not load dependency source", ex);
+            }
+            buildDependencyMap(this.dependencyResult.getRoot(), new ArrayList<>(), new HashSet<>());
+            sourceMapping = new SourceMappingBuilder(dependencyResult, itemIndex).build();
+            return itemIndex;
+        }
+    }
+    
     /**
      * 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
@@ -141,17 +329,24 @@
         private final DependencyResult dependencyResult;
         private final VulnerabilityReport report;
 
-        /**
-         * Maps GAV -> dependency.
-         */
-        private Map<String, Dependency> dependencyMap;
+        // @GuardedBy(this) -- initialization only
+        private Map<String, VulnerabilityItem> itemIndex;
         
+        // @GuardedBy(this) -- initialization only
+        private SourceMapping sourceMapping;
+        /**
+         * Number of dependencies
+         */
+        // @GuardedBy(this) -- initialization only
+        private int uniqueDependencies;
+
         // @GuardedBy(this)
         private ChangeListener depChange;
+        private ChangeListener sourceChange;
         
         // @GuardedBy(this)
         private RequestProcessor.Task pendingRefresh;
-
+        
         public CacheItem(Project project, DependencyResult dependency, VulnerabilityReport report) {
             this.project = project;
             this.dependencyResult = dependency;
@@ -169,52 +364,76 @@
         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 (gav != null && result.putIfAbsent(gav, dependency) == null) {
-                dependency.getChildren().forEach((childDependency) -> {
-                    buildDependecyMap(childDependency, result);
-                });
+        private synchronized void initialize() {
+            if (itemIndex != null) {
+                return;
             }
+            startListening();
+            CacheDataBuilder b = new CacheDataBuilder(dependencyResult, report);
+            Map<String, VulnerabilityItem> items = b.build();
+            this.itemIndex = b.itemIndex;
+            this.sourceMapping = b.sourceMapping;
+            this.uniqueDependencies =  b.uniqueDeps.size();
         }
         
         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.
-                        }
-                    }
+            initialize();
+            return sourceMapping.files;
+        }
+        
+        VulnerabilityItem findVulnerability(String gav) {
+            initialize();
+            return itemIndex.get(gav);
+        }
+        
+        void refreshSourceMapping(RequestProcessor.Task t) {
+            SourceMapping old;
+            SourceMapping m = new SourceMappingBuilder(dependencyResult, itemIndex).build();
+            Set<FileObject> files = new HashSet<>();
+            synchronized (this) {
+                // should block on this until t[0] is assigned
+                if (pendingRefresh != t) {
+                    return;
                 }
+                old = this.sourceMapping;
+                sourceMapping = m;
             }
-            return result;
+            if (old != null) {
+                files.addAll(old.files);
+            }
+            Diagnostic.ReporterControl reporter = Diagnostic.findReporterControl(Lookup.getDefault(), project.getProjectDirectory());
+            files.addAll(m.files);
+            reporter.diagnosticChanged(files, null);
         }
         
         void refreshDependencies(RequestProcessor.Task t) {
+            boolean model;
+            synchronized (cache) {
+                CacheItem registered = cache.get(project);
+                if (registered != this) {
+                    return;
+                }
+            }
+            synchronized (this) {
+                // should block on this until t[0] is assigned
+                if (pendingRefresh != t) {
+                    return;
+                }
+                model = modelChanged || itemIndex == null;
+                modelChanged = false;
+            }
+            if (model) {
+                refreshProjectDependencies(t);
+            } else {
+                refreshSourceMapping(t);
+            }
+        }
+        
+        void refreshProjectDependencies(RequestProcessor.Task t) {
             DependencyResult dr = ProjectDependencies.findDependencies(project, ProjectDependencies.newQuery(Scopes.RUNTIME));
             LOG.log(Level.FINER, "{0} - dependencies refreshed", this);
             synchronized (this) {
@@ -222,12 +441,13 @@
                 if (pendingRefresh != t) {
                     return;
                 }
+                modelChanged = false;
             }
             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)) {
+            if (replaceCacheItem(project, this, novy)) {
                 novy.startListening();
                 Diagnostic.ReporterControl reporter = Diagnostic.findReporterControl(Lookup.getDefault(), project.getProjectDirectory());
                 Set<FileObject> allFiles = new HashSet<>();
@@ -236,16 +456,31 @@
                 if (LOG.isLoggable(Level.FINER)) {
                     LOG.log(Level.FINER, "{0} - refreshing files: {1}", new Object[] { this, allFiles });
                 }
-                reporter.diagnosticChanged(novy.getProblematicFiles(), null);
+                reporter.diagnosticChanged(allFiles, null);
             }
         }
         
+        /**
+         * True, if the project model changed; false, if only source has changed.
+         */
+        private boolean modelChanged;
+        
+        void scheduleSourceRefresh(ChangeEvent e) {
+            // PENDING: enable when Maven and gradle dependency implementation stabilizaes on reading the 
+            // actual source.
+            // scheduleRefresh(false);
+        }
+        
         void scheduleDependencyRefresh(ChangeEvent e) {
+            scheduleRefresh(true);
+        }
+        
+        private void scheduleRefresh(boolean modelChanged) {
             synchronized (this) {
                 if (pendingRefresh != null) {
                     pendingRefresh.cancel();
                 }
-                
+                modelChanged |= modelChanged;
                 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 });
@@ -261,15 +496,12 @@
             }
         }
         
-        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);
+                    dependencyResult.addSourceChangeListener(sourceChange = this::scheduleSourceRefresh);
                 }
             }
         }
@@ -280,13 +512,38 @@
                     dependencyResult.removeChangeListener(depChange);
                     // intentionally does not clean depChange, to make a subsequent startListening no-op.
                     LOG.log(Level.FINER, "{0} - stop listen for dependencies", this);
+                    dependencyResult.removeSourceChangeListener(sourceChange);
                 }
             }
         }
         
-        SourceLocation getDependencyRange(Dependency d, String part) throws IOException {
-            startListening();
-            return dependencyResult.getDeclarationRange(d, part);
+        Diagnostic createDiagnostic(Dependency owner, Dependency dependency, List<Dependency> path, Vulnerability vulnerability, SourceLocation declarationRange) {
+            String ownerGav = owner == null ? null : getDependencyId(owner);
+            String message = 
+                ownerGav == null ? 
+                    Bundle.MSG_Diagnostic(
+                        formatCvssScore(vulnerability.getCvssV2Score()), 
+                        formatCvssScore(vulnerability.getCvssV3Score()), 
+                        getDependencyId(dependency)) :
+                    Bundle.MSG_Diagnostic_Included(
+                        formatCvssScore(vulnerability.getCvssV2Score()), 
+                        formatCvssScore(vulnerability.getCvssV3Score()), 
+                        getDependencyId(dependency),
+                        ownerGav);
+            SourceLocation fDeclarationRange = declarationRange;
+            Diagnostic.Builder builder = Diagnostic.Builder.create(() -> fDeclarationRange.getStartOffset(), () -> fDeclarationRange.getEndOffset(), message);
+            builder.setSeverity(Diagnostic.Severity.Warning);
+            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());
+            }
+            
+            // TODO: when Diagnostic supports (some) ID, change to report the unique id separately, now it is embedded into the diagnostic code.
+            String pathString = path.stream().map(d -> getDependencyId(d)).collect(Collectors.joining("/"));
+            builder.setCode(vulnerability.getId() + "~~" + pathString);
+            return builder.build();
         }
         
         public List<Diagnostic> getDiagnosticsForFile(FileObject file) {
@@ -297,69 +554,68 @@
                 return null;
             }
             
+            initialize();
+            
             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;
+            SourceLocation containerLocation = null;
+            
+            for (VulnerabilityItem item : this.itemIndex.values()) {
+                boolean unreported = true;
+                Set<Dependency> anchors = new HashSet<>();
+                for (List<Dependency> path : item.paths) {
+                    Dependency dependency = path.get(path.size() - 1);
                     
-                    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 });
-                            }
+                    SourceLocation declarationRange = null;
 
-                            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
-                        }                            
+                    String ownerGav = null;                    
+                    Dependency withSource = sourceMapping.nodesWithSource.get(dependency);
+                    if (withSource != null) {
+                        declarationRange = sourceMapping.locations.get(withSource); // XXX, neprepisovat nullem
+                    }
+                    if (declarationRange == null) {
+                        // will deal with unampped vulnerabilities later.
+                        continue;
+                    }
+                    Dependency owner = withSource;
+                    if (withSource != dependency) {
+                        // the dependency result may not report implied sources, so fallback
+                        // to the closest parent artifact
+                        ownerGav = getDependencyId(withSource);
                     }
 
-                    if (declarationRange != null && declarationRange.hasPosition() && declarationRange.getFile().equals(file)) {
-                        final SourceLocation fDeclarationRange = declarationRange;
-                        String ownerGav = null;
-                        if (fDeclarationRange.getImpliedBy() instanceof Dependency) {
-                            Dependency owner = (Dependency)fDeclarationRange.getImpliedBy();
-                            ownerGav = createGAV(owner.getArtifact());
-                        }
-                        for(Vulnerability vulnerability: vulnerabilities) {
-                            String message = 
-                                    ownerGav == null ? 
-                                        Bundle.MSG_Diagnostic(
-                                            formatCvssScore(vulnerability.getCvssV2Score()), 
-                                            formatCvssScore(vulnerability.getCvssV3Score()), 
-                                            createGAV(dependency.getArtifact())) :
-                                        Bundle.MSG_Diagnostic_Included(
-                                            formatCvssScore(vulnerability.getCvssV2Score()), 
-                                            formatCvssScore(vulnerability.getCvssV3Score()), 
-                                            createGAV(dependency.getArtifact()),
-                                            ownerGav);
-                            Diagnostic.Builder builder = Diagnostic.Builder.create(() -> fDeclarationRange.getStartOffset(), () -> fDeclarationRange.getEndOffset(), message);
-                            builder.setSeverity(Diagnostic.Severity.Warning).setCode(vulnerability.getId());
+                    if (declarationRange.getImpliedBy() instanceof Dependency) {
+                        owner = (Dependency)declarationRange.getImpliedBy();
+                        ownerGav = getDependencyId(owner);
+                    }
+                    if (!anchors.add(owner)) {
+                        // do not report additional "foo" included from "bar", if already reported.
+                        continue;
+                    }
+                    for (Vulnerability vulnerability : item.reports) {
+                        unreported = false;
+                        result.add(createDiagnostic(owner, dependency, path, vulnerability, declarationRange));
+                    }
+                }
+                if (unreported && !item.paths.isEmpty()) {
+                    // if the vulnerability item was matched initially and now it is not found, or mapped to a source,
+                    // the user may have removed it from the project without running the analysis again - it should not be reported, it will eventually reappear
+                    // after next analysis.
+                    // But if it is not in the original map for some reason, report on the container location
+                    if (!report.getMappedVulnerabilities().contains(item.gav)) {
+                        if (containerLocation == null) {
                             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());
+                                containerLocation = dependencyResult.getDeclarationRange(dependencyResult.getRoot(), DependencyResult.PART_CONTAINER);
+                            } catch (IOException ex) {
+                                LOG.log(Level.WARNING, "Could not load container location", ex);
                             }
-                            result.add(builder.build());
+                            if (containerLocation == null) {
+                                containerLocation = new SourceLocation(project.getProjectDirectory(), 0, 0, null);
+                            }
+                        }
+                        for (Vulnerability vulnerability : item.reports) {
+                            List<Dependency> path = item.paths.iterator().next();
+                            Dependency dependency = path.get(path.size() - 1);
+                            result.add(createDiagnostic(null, dependency, path, vulnerability, containerLocation));
                         }
                     }
                 }
@@ -375,18 +631,25 @@
         }
     }
     
-    private static boolean replaceCacheItem(CacheItem old, CacheItem novy) {
+    private static boolean replaceCacheItem(Project p, CacheItem novy) {
+        return replaceCacheItem(p, null, novy);
+    }
+    
+    private static boolean replaceCacheItem(Project p, CacheItem old, CacheItem novy) {
+        CacheItem registered;
         synchronized (cache) {
-            CacheItem registered = cache.get(old.project);
-            if (old != null && registered != old) {
-                old.stopListening();
-                return false;
+            registered = cache.get(p);
+            if (old != null) {
+                if (old != registered) {
+                    old.stopListening();
+                    return false;
+                }
+            } 
+            if (registered != null) {
+                registered.stopListening();
             }
             cache.put(novy.project, novy);
         }
-        if (old != null) {
-            old.stopListening();
-        }
         return true;
     }
     
@@ -549,7 +812,7 @@
 
         if (cacheItem != null) {
             synchronized (cache) {
-                cache.put(project, cacheItem);
+                replaceCacheItem(project, cacheItem);
             }
 
             Set<FileObject> problematicFiles = new HashSet<>();
@@ -572,8 +835,17 @@
             reporter.diagnosticChanged(problematicFiles, null);
             
             List<ArtifactSpec> arts = new ArrayList<>();
+            Set<String> gavs = new HashSet<>();
             for (ApplicationDependencyVulnerabilitySummary s : cacheItem.getVulnerabilities()) {
-                Dependency d = cacheItem.getDependencyMap().get(s.getGav());
+                if (!gavs.add(s.getGav())) {
+                    continue;
+                }
+                VulnerabilityItem i = cacheItem.findVulnerability(s.getGav());
+                if (i == null || i.paths.isEmpty()) {
+                    continue;
+                }
+                List<Dependency> p = i.paths.iterator().next();
+                Dependency d = p.get(p.size() - 1);
                 if (d == null) {
                     continue;
                 }
@@ -583,7 +855,7 @@
                 }
             }
             AuditResult res = new AuditResult(project, projectDisplayName, cacheItem.report.summary.getId(), 
-                    cacheItem.getDependencyMap().size(), cacheItem.getAudit().getVulnerableArtifactsCount(), 
+                    cacheItem.uniqueDependencies, cacheItem.getAudit().getVulnerableArtifactsCount(), 
                     arts);
             return res;
         } else {
@@ -630,9 +902,14 @@
         }
         // use a random constant that could be sufficient to hold gav text (micronaut-core has max 86)
         StringBuilder sb = new StringBuilder(120); 
-        sb.append(artifact.getGroupId()).append(':');
-        sb.append(artifact.getArtifactId()).append(':');
-        sb.append(artifact.getVersionSpec());
+        sb.append(String.format("%s:%s:%s", 
+                nonNull(artifact.getGroupId()), 
+                nonNull(artifact.getArtifactId()),
+                nonNull(artifact.getVersionSpec()))
+        );
+        if (artifact.getClassifier() != null) {
+            sb.append(':').append(artifact.getClassifier());
+        }
         return sb.toString();
     }
 
@@ -691,8 +968,8 @@
         for (ApplicationDependencyVulnerabilitySummary v : items) {
             List<Vulnerability> vulnerabilities = v.getVulnerabilities();
             if (!vulnerabilities.isEmpty()) {
-                Dependency dependency = cache.getDependencyMap().get(v.getGav());
-                if (dependency != null) {
+                VulnerabilityItem item = cache.findVulnerability(v.getGav());
+                if (item != null && !item.paths.isEmpty()) {
                     mapped.add(v.getGav());
                 }
             }
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java
index 0dd12e5..782ef48 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java
@@ -2055,7 +2055,14 @@
                     case Information: diag.setSeverity(DiagnosticSeverity.Information); break;
                     default: throw new IllegalStateException("Unknown severity: " + err.getSeverity());
                 }
-                diag.setCode(id2Error.getKey());
+                // TODO: currently Diagnostic.getCode() is misused to provide an unique ID for the diagnostic. This should be changed somehow
+                // at SPI level between ErrorProvider and LSP core. For now, report just part of the (mangled) diagnostics code.
+                String realCode = id2Error.getKey();
+                int idPart = realCode == null ?  -1 : realCode.indexOf("~~"); // NOI18N
+                if (idPart != -1) {
+                    realCode = realCode.substring(0, idPart);
+                }
+                diag.setCode(realCode);
                 result.add(diag);
             }
             if (offset >= 0) {