利用Sonatype API實(shí)現(xiàn)依賴檢測工具
背景
由于畢設(shè)答辯時(shí)間提前,導(dǎo)致迫不得已砍掉部分功能,而個(gè)人感覺功能有點(diǎn)少故添加了個(gè)依賴檢測功能,大體原理利用Maven API解析提取上傳的Pom文件中的依賴項(xiàng),然后借助Sonatype進(jìn)行安全檢測。
依賴項(xiàng)提取
其實(shí)這里實(shí)現(xiàn)方式多種,模仿XmlDecoder的方式自定義DocumentHandler雖然可以(Tomcat內(nèi)部解析web.xml就是這種方式)不過對于部分把版本號以變量形式約束定義在Properties標(biāo)簽的情況處理比較麻煩,故采用Maven API,它可以直接提取properties中的標(biāo)簽值等。
依賴
<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>
代碼實(shí)現(xiàn)
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 {
????//?參數(shù):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對象轉(zhuǎn)為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) { // 遍歷每個(gè)依賴對象
String version = dependency.getVersion();
if (version!=null && version.startsWith("${") && version.endsWith("}")) { // 如果版本號以${}包裹,則需要進(jìn)行替換
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結(jié)構(gòu)體
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,導(dǎo)致測試時(shí)候獲取不到數(shù)據(jù),查閱網(wǎng)上方案得以解決
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();
}
/**
* 發(fā)送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();
}
}
}
}
調(diào)用API檢測
Sonatype API:https://ossindex.sonatype.org/rest

可以看到參數(shù)主要是這個(gè)?coordinates?字段,關(guān)于它的格式定義(https://ossindex.sonatype.org/doc/coordinates)

如果是檢測Java項(xiàng)目則格式如下
maven:groupId/artifactId@Version
是以,我們可以進(jìn)行組合實(shí)現(xiàn)檢測依賴文件中的依賴項(xiàng)安全。

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 項(xiàng)目的所有依賴項(xiàng),并返回包含 CVE 信息的掃描結(jié)果。
*
* @param groupId Maven 項(xiàng)目的 Group ID
* @param artifactId Maven 項(xiàng)目的 Artifact ID
* @param version Maven 項(xiàng)目的版本號
* @return 包含 CVE 信息的掃描結(jié)果列表
*/
public List<ScanResult> scanDependencies(String groupId, String artifactId, String version) throws IOException, JSONException {
// 處理 CVE 數(shù)據(jù)并生成掃描結(jié)果
List<ScanResult> results = new ArrayList<>();
try{
// 構(gòu)造 API 請求 URL
String url = API_BASE_URL + "/component-report/";
String content="{ \"coordinates\":[\"maven:"+groupId+"/"+artifactId+"@"+version+"\"]}";
// 解析 JSON 響應(yīng)
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結(jié)構(gòu)體(自行生成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;
????}
}
需要注意的是這里有點(diǎn)坑,那就獲取到的數(shù)據(jù)包含了響應(yīng)頭數(shù)據(jù),故這里利用響應(yīng)體結(jié)構(gòu)特性進(jìn)行了較為暴力的處理,當(dāng)然也可以用其他方式實(shí)現(xiàn),這里個(gè)人偷懶。
int start = data.indexOf("[{");
data=data.substring(start+1,data.length()-1);
效果
以下為個(gè)人實(shí)現(xiàn)效果,這里沒有給出我控制器這邊代碼,可自行琢磨封裝使用。

結(jié)語
注意一下的是Sonatype并非只能檢測Java依賴項(xiàng)安全,它支持多種語言的依賴檢測,只不過這里以Java為例。

