利用Sonatype API实现依赖检测工具
    
      背景
    
    
由于毕设答辩时间提前,导致迫不得已砍掉部分功能,而个人感觉功能有点少故添加了个依赖检测功能,大体原理利用Maven API解析提取上传的Pom文件中的依赖项,然后借助Sonatype进行安全检测。
    
      
  
依赖项提取
其实这里实现方式多种,模仿XmlDecoder的方式自定义DocumentHandler虽然可以(Tomcat内部解析web.xml就是这种方式)不过对于部分把版本号以变量形式约束定义在Properties标签的情况处理比较麻烦,故采用Maven API,它可以直接提取properties中的标签值等。
    
  
    
      依赖
    
    
      
        
          <dependency>
        
      
      
          <groupId>org.apache.maven</groupId>
      
      
          <artifactId>maven-model-builder</artifactId>
      
      
          <version>3.8.1</version>
      
      
        
          </dependency>
        
      
      
        
          <dependency>
        
      
      
          <groupId>org.apache.maven</groupId>
      
      
          <artifactId>maven-model</artifactId>
      
      
           <version>3.8.1</version>
      
      
        
          </dependency>
        
      
    
  
    
      代码实现
    
    
      
        import java.io.File;
      
      
        import java.io.FileReader;
      
      
        import java.io.StringReader;
      
      
        import java.io.StringWriter;
      
      
        import java.util.ArrayList;
      
      
        import java.util.List;
      
      
        import java.util.Properties;
      
      
        import java.util.regex.Matcher;
      
      
        import java.util.regex.Pattern;
      
      
        
          
      
      
        import javax.xml.parsers.DocumentBuilder;
      
      
        import javax.xml.parsers.DocumentBuilderFactory;
      
      
        
          
      
      
        import org.apache.maven.model.Dependency;
      
      
        import org.apache.maven.model.DependencyManagement;
      
      
        import org.apache.maven.model.Model;
      
      
        import org.apache.maven.model.io.DefaultModelWriter;
      
      
        import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
      
      
        import org.w3c.dom.Document;
      
      
        import org.w3c.dom.Element;
      
      
        import org.w3c.dom.NodeList;
      
      
        
          
      
      
        public class PomParser {
      
      
            // 参数:Pom文件绝对路径
      
      
            public static List<Dependency> parse(String pomPath) throws Exception {
      
      
                List<Dependency> result = new ArrayList<>();
      
      
                MavenXpp3Reader reader = new MavenXpp3Reader();
      
      
                Model model = reader.read(new FileReader(pomPath));
      
      
        
          
      
      
                Properties properties = model.getProperties(); // 获取所有属性
      
      
                // 将model对象转为xml字符串
      
      
                DefaultModelWriter writer = new DefaultModelWriter();
      
      
                StringWriter stringWriter = new StringWriter();
      
      
                writer.write(stringWriter, null, model);
      
      
        
          
      
      
                String xmlString = stringWriter.toString();
      
      
        
          
      
      
                // 合并Dependencies和DependencyManagement
      
      
                List<org.apache.maven.model.Dependency> allDependencies=model.getDependencies();
      
      
                DependencyManagement dependencyManagement = model.getDependencyManagement();
      
      
                if (dependencyManagement!=null && dependencyManagement.getDependencies().size()>0){
      
      
                    allDependencies.addAll(dependencyManagement.getDependencies());
      
      
                }
      
      
                for (org.apache.maven.model.Dependency dependency : allDependencies) { // 遍历每个依赖对象
      
      
                    String version = dependency.getVersion();
      
      
        
          
      
      
                    if (version!=null && version.startsWith("${") && version.endsWith("}")) { // 如果版本号以${}包裹,则需要进行替换
      
      
                        String propertyName = version.substring(2, version.length() - 1);
      
      
                        String propertyValue = properties.getProperty(propertyName);
      
      
        
          
      
      
                        if (propertyValue == null) { // 如果属性不存在,抛出异常
      
      
                            throw new IllegalArgumentException("Property not found: " + propertyName);
      
      
                        }
      
      
                        dependency.setVersion(propertyValue);
      
      
                    }
      
      
                }
      
      
        
          
      
      
                for (org.apache.maven.model.Dependency dependency : allDependencies) {
      
      
                    result.add(new Dependency(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion()));
      
      
                }
      
      
                return result;
      
      
            }
      
      
        }
      
      
        
          
      
      
        
          
      
      
        
          
      
      
        
          // Denpendency结构体
        
      
      
            public static class Dependency {
      
      
                private final String groupId;
      
      
                private final String artifactId;
      
      
                private final String version;
      
      
        
          
      
      
                public Dependency(String groupId, String artifactId, String version) {
      
      
                    this.groupId = groupId;
      
      
                    this.artifactId = artifactId;
      
      
                    this.version = version;
      
      
                }
      
      
        
          
      
      
                public String getGroupId() {
      
      
                    return groupId;
      
      
                }
      
      
        
          
      
      
                public String getArtifactId() {
      
      
                    return artifactId;
      
      
                }
      
      
        
          
      
      
                public String getVersion() {
      
      
                    return version;
      
      
                }
      
      
            }
      
    
  
    
    
    
      Https问题
    
    
由于Sonatype的API为HTTPS,导致测试时候获取不到数据,查阅网上方案得以解决
      
        package com.VulnScanner.DpendCheck;
      
      
        
          
      
      
        
          
      
      
        import org.apache.http.HttpEntity;
      
      
        import org.apache.http.client.methods.CloseableHttpResponse;
      
      
        import org.apache.http.client.methods.HttpPost;
      
      
        import org.apache.http.conn.ssl.NoopHostnameVerifier;
      
      
        import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
      
      
        import org.apache.http.entity.StringEntity;
      
      
        import org.apache.http.impl.client.CloseableHttpClient;
      
      
        import org.apache.http.impl.client.HttpClients;
      
      
        import org.apache.http.ssl.SSLContextBuilder;
      
      
        import org.apache.http.ssl.TrustStrategy;
      
      
        import org.apache.http.util.EntityUtils;
      
      
        
          
      
      
        import javax.net.ssl.HostnameVerifier;
      
      
        import javax.net.ssl.SSLContext;
      
      
        import java.io.IOException;
      
      
        import java.security.KeyManagementException;
      
      
        import java.security.KeyStoreException;
      
      
        import java.security.NoSuchAlgorithmException;
      
      
        import java.security.cert.CertificateException;
      
      
        import java.security.cert.X509Certificate;
      
      
        
          
      
      
        public class HttpsUtils {
      
      
            static CloseableHttpClient httpClient;
      
      
            static CloseableHttpResponse httpResponse;
      
      
        
          
      
      
            public static CloseableHttpClient createSSLClientDefault() {
      
      
                try {
      
      
                    SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
      
      
                        // 信任所有
      
      
                        public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
      
      
                            return true;
      
      
                        }
      
      
                    }).build();
      
      
                    HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
      
      
                    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
      
      
                    return HttpClients.custom().setSSLSocketFactory(sslsf).build();
      
      
                } catch (KeyManagementException e) {
      
      
                    e.printStackTrace();
      
      
                } catch (NoSuchAlgorithmException e) {
      
      
                    e.printStackTrace();
      
      
                } catch (KeyStoreException e) {
      
      
                    e.printStackTrace();
      
      
                }
      
      
                return HttpClients.createDefault();
      
      
        
          
      
      
            }
      
      
        
          
      
      
            /**
      
      
        
               * 发送https请求
        
      
      
        
               *
        
      
      
        
               * @param content
        
      
      
        
               * @throws Exception
        
      
      
        
               */
        
      
      
            public static String send(String content, String url) {
      
      
                try {
      
      
                    HttpPost request = new HttpPost(url);
      
      
                    StringEntity entity = new StringEntity(content, "UTF-8");
      
      
                    entity.setContentEncoding("UTF-8");
      
      
                    entity.setContentType("application/json");
      
      
                    request.setEntity(entity);
      
      
                    request.addHeader("Connection", "Keep-Alive");
      
      
                    request.addHeader("accept", "application/json");
      
      
                    request.addHeader("Content-Type", "application/json");
      
      
                    httpClient = HttpsUtils.createSSLClientDefault();
      
      
                    httpResponse = httpClient.execute(request);
      
      
                    HttpEntity httpEntity = httpResponse.getEntity();
      
      
                    if (httpEntity != null) {
      
      
                        String jsObject = EntityUtils.toString(httpEntity, "UTF-8");
      
      
                        return jsObject;
      
      
                    } else {
      
      
                        return null;
      
      
                    }
      
      
                } catch (Exception e) {
      
      
                    e.printStackTrace();
      
      
                    return null;
      
      
                } finally {
      
      
                    try {
      
      
                        httpResponse.close();
      
      
                        httpClient.close();
      
      
                    } catch (IOException e) {
      
      
                        e.printStackTrace();
      
      
                    }
      
      
                }
      
      
            }
      
      
        }
      
    
  
    
    
调用API检测
    Sonatype API:https://ossindex.sonatype.org/rest
    
    
    可以看到参数主要是这个 coordinates 字段,关于它的格式定义(https://ossindex.sonatype.org/doc/coordinates)
    
    
    如果是检测Java项目则格式如下
  
      
        maven:groupId/artifactId@Version
      
    
  是以,我们可以进行组合实现检测依赖文件中的依赖项安全。
    
    
  
      
        public class Checker {
      
      
            private static final String API_BASE_URL = "https://ossindex.sonatype.org/api/v3";
      
      
        
          
      
      
        
          
      
      
            public  List<ScanResult> getAllVulns(String filePath) throws Exception {
      
      
                List<ScanResult> scanResults=new ArrayList<ScanResult>();
      
      
                try {
      
      
                    List<PomParser.Dependency> dependencies = PomParser.parse(filePath);
      
      
                    for (PomParser.Dependency dependency:dependencies){
      
      
                        scanResults.addAll(scanDependencies(dependency.getGroupId(),dependency.getArtifactId(),dependency.getVersion()));
      
      
                    }
      
      
                }catch (Exception e){}
      
      
        
          
      
      
                return scanResults;
      
      
            }
      
      
        
          
      
      
        
          
      
      
        
          
      
      
        
          
      
      
            /**
      
      
        
               * 扫描指定 Maven 项目的所有依赖项,并返回包含 CVE 信息的扫描结果。
        
      
      
        
               *
        
      
      
        
               * @param groupId Maven 项目的 Group ID
        
      
      
        
               * @param artifactId Maven 项目的 Artifact ID
        
      
      
        
               * @param version Maven 项目的版本号
        
      
      
        
               * @return 包含 CVE 信息的扫描结果列表
        
      
      
        
               */
        
      
      
            public List<ScanResult> scanDependencies(String groupId, String artifactId, String version) throws IOException, JSONException {
      
      
                // 处理 CVE 数据并生成扫描结果
      
      
                List<ScanResult> results = new ArrayList<>();
      
      
                try{
      
      
                    // 构造 API 请求 URL
      
      
                    String url = API_BASE_URL + "/component-report/";
      
      
                    String content="{ \"coordinates\":[\"maven:"+groupId+"/"+artifactId+"@"+version+"\"]}";
      
      
                    // 解析 JSON 响应
      
      
                    String data = HttpsUtils.send(content, url);
      
      
                    int start = data.indexOf("[{");
      
      
                    data=data.substring(start+1,data.length()-1);
      
      
                    JSONObject jsonResponse = new JSONObject(data);
      
      
                    JSONArray vulnerabilities = jsonResponse.optJSONArray("vulnerabilities");
      
      
        
          
      
      
                    if (vulnerabilities != null) {
      
      
                        for (int i = 0; i < vulnerabilities.length(); i++) {
      
      
                            JSONObject vuln = vulnerabilities.getJSONObject(i);
      
      
                            String cveId = vuln.optString("cve");
      
      
                            String description = vuln.optString("description");
      
      
                            results.add(new ScanResult(cveId, description,groupId,artifactId,version));
      
      
                        }
      
      
                    }
      
      
                }catch (Exception e){
      
      
        
          
      
      
                }
      
      
                return results;
      
      
            }
      
      
        }
      
      
        
          
      
      
        
          
      
      
        
          
      
      
        
          // ScanResult结构体(自行生成Getter/Setter)
        
      
      
        public class ScanResult {
      
      
            private String cveId;
      
      
            private String description;
      
      
            private String groupId;
      
      
            private String artifactId;
      
      
            private String version;
      
      
        
          
      
      
            public ScanResult(String cveId, String description,String groupId,String artifactId,String version) {
      
      
                this.cveId = cveId;
      
      
                this.description = description;
      
      
                this.groupId=groupId;
      
      
                this.artifactId=artifactId;
      
      
                this.version=version;
      
      
            }
      
      
        }
      
    
  需要注意的是这里有点坑,那就获取到的数据包含了响应头数据,故这里利用响应体结构特性进行了较为暴力的处理,当然也可以用其他方式实现,这里个人偷懒。
      
        int start = data.indexOf("[{");
      
      
        data=data.substring(start+1,data.length()-1);
      
    
  
    
    
效果
以下为个人实现效果,这里没有给出我控制器这边代码,可自行琢磨封装使用。
    
    
    
结语
注意一下的是Sonatype并非只能检测Java依赖项安全,它支持多种语言的依赖检测,只不过这里以Java为例。
    
    
