链盾shieldchain | DID工具类、加密解密工具类、为用户上传软件包生成三种格式SBOM、漏洞清单、线程池
由于需要为软件和用户都生成DID,所以将这一部分代码抽取出来,单独作为一个工具类。原先代码由于删掉了user实体的update_user和create_user,所以部分sql和代码发生变化,注册部分代码以此次commit为准。
DID工具类
由于需要为软件和用户都生成DID,所以将这一部分代码抽取出来,单独作为一个工具类。
原先代码
package com.sky.service.impl;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.UserLoginDTO;
import com.sky.dto.UserRegisterDTO;
import com.sky.entity.User;
import com.sky.exception.*;
import com.sky.mapper.UserMapper;
import com.sky.service.UserService;
import io.lettuce.core.codec.Base16;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.Base58;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.Base64;
@Service
@Slf4j
public class UserServiceImpl implements UserService{
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
private static final int IV_LENGTH = 16; // 128位
@Autowired
private UserMapper userMapper;
/**
* 用户登录
* @param userLoginDTO
* @return
*/
public User login(UserLoginDTO userLoginDTO) {
String username = userLoginDTO.getUsername();
String password = userLoginDTO.getPassword();
//1. 根据用户名查询数据库中的数据
User user = userMapper.getByUsername(username);
//2. 处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (user == null){
// 账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
// 密码比对
// TODO 后期需要进行md5加密,然后再进行比对
if (!password.equals(user.getPassword())){
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (user.getStatus() == StatusConstant.DISABLE){
// 账户被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
// 3. 返回实体对象
return user;
}
/**
* 新增用户--用户注册
* 生成公私钥对(椭圆曲线加密算法ECC)、基于公钥生成did标识符
* @param userRegisterDTO
* @return
*/
@Transactional
public boolean register(UserRegisterDTO userRegisterDTO) {
// 1.校验用户名是否已经存在(避免重复注册)
User existUser = userMapper.getByUsername(userRegisterDTO.getUsername());
if (existUser != null){
//用户已经存在
throw new AccountAlreadyExistException(MessageConstant.ALREADY_EXISTS);
}
//2. 校验密码与确认密码是否一致(前端已校验,后端二次保障)
if (!userRegisterDTO.getPassword().equals(userRegisterDTO.getConfirmPassword())){
throw new PasswordErrorException(MessageConstant.PASSWORD_NOT_EQUAL);
}
User user = new User();
// BeanUtils只拷贝两个对象中“字段名和类型都相同” 的属性:username,password
BeanUtils.copyProperties(userRegisterDTO, user);
// 1. 生成ECC密钥对
KeyPair keyPair = generateECCKeyPair();
// 2. 从KeyPair中获取公钥和私钥
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// 3. 基于公钥生成DID标识符
String didIdentifier = generateDidFromPublicKey(publicKey);
// 4. 构建完整的DID字符串
String fullDid = "did:shieldchain:user:" + didIdentifier;
//设置剩余属性
user.setDid(fullDid);
user.setStatus(StatusConstant.ENABLE);
user.setPublicKey(Base64.getEncoder().encodeToString(publicKey.getEncoded()));
// 私钥加密存储
String encryptedPrivateKey = encryptWithPassword(Base64.getEncoder().encodeToString(privateKey.getEncoded()), user.getPassword());
user.setPrivateKeyEncrypted(encryptedPrivateKey);
userMapper.insertUser(user);
return true;
}
/**
* 生成ECC密钥对
* @return
*/
private KeyPair generateECCKeyPair(){
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
// 替换为Java支持的标准曲线(secp256r1)
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
keyPairGenerator.initialize(ecSpec, new SecureRandom());// 使用强随机数
return keyPairGenerator.generateKeyPair();
} catch (Exception e) {
// 打印详细异常信息,方便排查
e.printStackTrace();
throw new KeyPairGenException("为用户生成ECC密钥对失败");
}
}
/**
* 根据生成的公钥两次哈希得到DID
* @param publicKey
* @return
*/
private String generateDidFromPublicKey(PublicKey publicKey){
try {
// 注册BouncyCastle加密提供者(支持RIPEMD-160)
// Security.addProvider(new BouncyCastleProvider());
// 获取公钥的原始编码
byte[] publicKeyBytes = publicKey.getEncoded();
// 计算SHA-256哈希,得到256位,32字节
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] hash1 = sha256.digest(publicKeyBytes);
// 计算RIPEMD-160哈希(指定使用BouncyCastle提供者)
// MessageDigest ripemd160 = MessageDigest.getInstance("RIPEMD160", "BC");
//byte[] hash2 = ripemd160.digest(hash1);
// 3. 转为十六进制字符串(Base16编码,全小写)
StringBuilder hexBuilder = new StringBuilder();
for (byte b : hash1) {
// %02x 表示:按2位十六进制小写输出,不足2位补0(确保每个字节对应2个字符)
hexBuilder.append(String.format("%02x", b));
}
// 4. 最终结果为64位全小写十六进制字符串
return hexBuilder.toString();
} catch (Exception e) {
// 打印完整异常信息,方便排查(如算法名错误、依赖未引入)
e.printStackTrace();
throw new DidGenException("为用户生成DID标识符失败");
}
}
/**
* 使用用户密码加密数据
* @param plainText 待加密的明文
* @param password 用户密码
* @return Base64编码的加密结果(包含IV)
*/
public static String encryptWithPassword(String plainText, String password) {
try {
// 1. 从密码生成AES密钥(使用SHA-256哈希)
byte[] key = generateKeyFromPassword(password);
SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
// 2. 生成随机IV
byte[] iv = new byte[IV_LENGTH];
java.security.SecureRandom random = new java.security.SecureRandom();
random.nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
// 3. 初始化加密器并执行加密
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// 4. 组合IV和加密数据,并进行Base64编码
byte[] combined = new byte[iv.length + encryptedBytes.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encryptedBytes, 0, combined, iv.length, encryptedBytes.length);
return Base64.getEncoder().encodeToString(combined);
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
/**
* 使用用户密码解密数据
* @param encryptedText Base64编码的加密数据(包含IV)
* @param password 用户密码
* @return 解密后的明文
*/
public static String decryptWithPassword(String encryptedText, String password) {
try {
// 1. 从密码生成AES密钥
byte[] key = generateKeyFromPassword(password);
SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
// 2. 解码并分离IV和加密数据
byte[] combined = Base64.getDecoder().decode(encryptedText);
byte[] iv = new byte[IV_LENGTH];
byte[] encryptedBytes = new byte[combined.length - IV_LENGTH];
System.arraycopy(combined, 0, iv, 0, iv.length);
System.arraycopy(combined, iv.length, encryptedBytes, 0, encryptedBytes.length);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
// 3. 初始化解密器并执行解密
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("解密失败", e);
}
}
/**
* 从密码生成AES密钥(SHA-256哈希)
*/
private static byte[] generateKeyFromPassword(String password) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(password.getBytes(StandardCharsets.UTF_8));
}
}
由于删掉了user实体的update_user和create_user,所以部分sql和代码发生变化,注册部分代码以此次commit为准
package com.sky.utils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.Base64;
/**
* DID工具类 - 用于生成符合W3C标准的分布式身份标识
* 采用工具类模式,提供静态方法调用
*/
//@Component
public class DIDUtil {
// 私有构造函数,防止实例化
private DIDUtil() {
throw new AssertionError("DIDUtil是一个工具类,不应被实例化");
}
// DID前缀常量
public static final String DID_PREFIX_USER = "did:shieldchain:user:";
public static final String DID_PREFIX_SOFTWARE = "did:shieldchain:software:";
public static final String ECC_ALGORITHM = "EC";
public static final String ECC_CURVE = "secp256r1";
public static final String HASH_ALGORITHM = "SHA-256";
/**
* 生成ECC密钥对
* 使用单例模式确保线程安全[6](@ref)
*
* @return 生成的密钥对
* @throws NoSuchAlgorithmException
* @throws InvalidAlgorithmParameterException
*/
public static KeyPair generateECCKeyPair() throws Exception {
// 使用静态方法,避免状态保存[7](@ref)
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ECC_ALGORITHM);
ECGenParameterSpec ecSpec = new ECGenParameterSpec(ECC_CURVE);
keyPairGenerator.initialize(ecSpec, new SecureRandom());// 使用强随机数
return keyPairGenerator.generateKeyPair();
}
/**
* 根据公钥生成DID标识符(核心方法)
* 符合W3C DID标准生成规则[1](@ref)
*
* @param publicKey 公钥对象
* @return DID标识符字符串
* @throws NoSuchAlgorithmException
*/
public static String generateDidIdentifierFromPublicKey(PublicKey publicKey) throws NoSuchAlgorithmException {
// 获取公钥的原始编码
byte[] publicKeyBytes = publicKey.getEncoded();
// 计算SHA-256哈希,得到256位(32字节)哈希值
MessageDigest sha256 = MessageDigest.getInstance(HASH_ALGORITHM);
byte[] hash = sha256.digest(publicKeyBytes);
// 转换为十六进制字符串(Base16编码,全小写)
StringBuilder hexBuilder = new StringBuilder();
for (byte b : hash) {
hexBuilder.append(String.format("%02x", b));
}
return hexBuilder.toString();
}
/**
* 为用户生成完整DID
*
* @param publicKey 用户公钥
* @return 完整的用户DID字符串
*/
public static String generateUserDID(PublicKey publicKey) {
try {
String didIdentifier = generateDidIdentifierFromPublicKey(publicKey);
return DID_PREFIX_USER + didIdentifier;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("生成用户DID失败", e);
}
}
/**
* 为软件生成完整DID
*
* @param publicKey 软件公钥
* @return 完整的软件DID字符串
*/
public static String generateSoftwareDID(PublicKey publicKey) {
try {
String didIdentifier = generateDidIdentifierFromPublicKey(publicKey);
return DID_PREFIX_SOFTWARE + didIdentifier;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("生成软件DID失败", e);
}
}
/**
* 生成完整的DID文档(基础版本)
* 可根据需要扩展为完整的DID文档结构
*
* @param did DID标识符
* @param publicKey 公钥
* @return 简化的DID文档JSON字符串
*/
public static String generateBasicDIDDocument(String did, PublicKey publicKey) {
String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
// 简化的DID文档结构,符合W3C标准
return String.format("{" +
"\"@context\": \"https://www.w3.org/ns/did/v1\"," +
"\"id\": \"%s\"," +
"\"verificationMethod\": [{" +
"\"id\": \"%s#keys-1\"," +
"\"type\": \"EcdsaSecp256r1VerificationKey2019\"," +
"\"controller\": \"%s\"," +
"\"publicKeyBase64\": \"%s\"" +
"}]" +
"}", did, did, did, publicKeyBase64);
}
/**
* 验证DID格式是否合法
*
* @param did 待验证的DID
* @return 是否合法
*/
public static boolean validateDIDFormat(String did) {
if (did == null || did.isEmpty()) {
return false;
}
// 基本的DID格式验证
return did.matches("^did:shieldchain:(user|software):[a-f0-9]{64}$");
}
/**
* 从DID中提取标识符部分
*
* @param fullDID 完整DID
* @return 标识符部分
*/
public static String extractIdentifierFromDID(String fullDID) {
if (!validateDIDFormat(fullDID)) {
throw new IllegalArgumentException("无效的DID格式");
}
int lastColonIndex = fullDID.lastIndexOf(":");
return fullDID.substring(lastColonIndex + 1);
}
/**
* 获取DID类型(用户或软件)
*
* @param did 完整DID
* @return 类型字符串
*/
public static String getDIDType(String did) {
if (!validateDIDFormat(did)) {
throw new IllegalArgumentException("无效的DID格式");
}
String[] parts = did.split(":");
if (parts.length >= 4) {
return parts[3]; // 返回"user"或"software"
}
return "unknown";
}
}
加密解密工具类
package com.sky.utils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
/**
* 简化加密工具类 - 用于用户和软件DID的加密需求
* 使用固定密钥,注重实用性而非最高安全性
*/
public class SimpleEncryptUtil {
// 私有构造函数
private SimpleEncryptUtil() {
throw new UnsupportedOperationException("工具类不应被实例化");
}
// 固定密钥(建议在配置文件中管理,此处为示例)
private static final String FIXED_KEY = "shieldchain_did_2025";
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
private static final int IV_LENGTH = 16;
/**
* 简化加密方法 - 使用固定密钥
* @param plainText 待加密文本
* @return Base64编码的加密结果
*/
public static String simpleEncrypt(String plainText) {
return encryptWithFixedKey(plainText, FIXED_KEY);
}
/**
* 简化解密方法 - 使用固定密钥
* @param encryptedText 加密文本
* @return 解密后的原文
*/
public static String simpleDecrypt(String encryptedText) {
return decryptWithFixedKey(encryptedText, FIXED_KEY);
}
/**
* 使用指定密钥加密
* @param plainText 待加密文本
* @param key 加密密钥
* @return Base64编码的加密结果
*/
public static String encryptWithKey(String plainText, String key) {
try {
// 1. 从密钥生成AES密钥
byte[] keyBytes = generateKeyFromString(key);
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
// 2. 生成固定IV(基于密钥的哈希,确保可重现)
byte[] iv = generateFixedIV(key);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
// 3. 执行加密
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// 4. 组合IV和加密数据(为保持兼容性,但使用固定IV时可选择不存储)
byte[] combined = new byte[iv.length + encryptedBytes.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encryptedBytes, 0, combined, iv.length, encryptedBytes.length);
return Base64.getEncoder().encodeToString(combined);
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
/**
* 使用指定密钥解密
* @param encryptedText 加密文本
* @param key 解密密钥
* @return 解密后的原文
*/
public static String decryptWithKey(String encryptedText, String key) {
try {
// 1. 从密钥生成AES密钥
byte[] keyBytes = generateKeyFromString(key);
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
// 2. 解码并分离数据
byte[] combined = Base64.getDecoder().decode(encryptedText);
// 如果数据包含IV,则分离;否则使用生成的固定IV
byte[] iv;
byte[] encryptedBytes;
if (combined.length > IV_LENGTH) {
iv = new byte[IV_LENGTH];
encryptedBytes = new byte[combined.length - IV_LENGTH];
System.arraycopy(combined, 0, iv, 0, iv.length);
System.arraycopy(combined, iv.length, encryptedBytes, 0, encryptedBytes.length);
} else {
// 兼容不包含IV的旧数据
iv = generateFixedIV(key);
encryptedBytes = combined;
}
IvParameterSpec ivSpec = new IvParameterSpec(iv);
// 3. 执行解密
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("解密失败", e);
}
}
/**
* 使用固定密钥加密(兼容原有方法签名)
*/
private static String encryptWithFixedKey(String plainText, String password) {
return encryptWithKey(plainText, password);
}
/**
* 使用固定密钥解密(兼容原有方法签名)
*/
private static String decryptWithFixedKey(String encryptedText, String password) {
return decryptWithKey(encryptedText, password);
}
/**
* 从字符串生成固定长度的密钥(SHA-256哈希)
*/
private static byte[] generateKeyFromString(String input) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
// AES-128需要16字节,取前16字节
byte[] key = new byte[16];
System.arraycopy(hash, 0, key, 0, 16);
return key;
}
/**
* 生成固定IV(基于密钥的MD5哈希前16字节)
*/
private static byte[] generateFixedIV(String key) throws Exception {
MessageDigest digest = MessageDigest.getInstance("MD5");
return digest.digest(key.getBytes(StandardCharsets.UTF_8));
}
/**
* 为软件DID提供的专用加密方法
*/
public static String encryptForSoftware(String plainText) {
return encryptWithKey(plainText, FIXED_KEY + "_software");
}
/**
* 为软件DID提供的专用解密方法
*/
public static String decryptForSoftware(String encryptedText) {
return decryptWithKey(encryptedText, FIXED_KEY + "_software");
}
}
software表、software_file_info表更新
为了操作方便,删除了did表,所有属性都移动到software表

修改实体类
package com.sky.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Software implements Serializable {
// 序列化机制的版本标识,用于反序列化时验证类的版本一致性(如果类结构发生变化,修改此值可以避免反序列化异常)
private static final long serialVersionUID = 1L;
private Integer id;
private String did;
private String name;
private String dependRate;
private String versions;
// 软件类型 0-开源软件,1-商业软件
private Integer softwareType;
//归属团队 ID,关联team.id
private Integer teamId;
private String language;
//DID状态:1-有效,2-冻结,0-吊销
private Integer didStatus;
// 任务处理状态:0-处理中,1-成功结束,2-失败
private Integer taskStatus;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer createUser;
private Integer updateUser;
// 新增
private String publicKey;
private String privateKeyEncrypt;
private Integer keyRotationCycle;
private LocalDateTime lastRotationTime;
}
software_file_info表:要注意漏洞相关字段不能设置为非空,因为一开始只插入SBOM相关字段。还要注意software_id不能设置为unique_key,因为为每一个软件生成三种格式的SBOM。

原先生成SBOM和漏洞清单的代码逻辑
package com.hby.shieldchaintest;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class SyftSbomTest {
/** 本地 syft 可执行文件路径 */
private final String SYFT_PATH = "E:\\syft\\syft.exe";
/** 上传的软件目录(测试用,可以换成任意上传目录) */
private final String UPLOAD_DIR = "E:\\OpenCV\\software\\kubernetes-master";
/** 生成 SBOM 保存路径 */
private final String OUT_FILE = "E:\\Java\\WebProject\\shieldChainTest\\src\\main\\resources\\mock\\sbom-syft.json";
@Test
void generateSbomForUploadedSoftware() throws Exception {
File uploadDir = new File(UPLOAD_DIR);
assertTrue(uploadDir.exists() && uploadDir.isDirectory(), "上传目录不存在或不是文件夹");
File sbomFile = generateSbomWithCpe(uploadDir, OUT_FILE);
assertTrue(sbomFile.exists(), "SBOM 文件应存在");
assertTrue(sbomFile.length() > 0, "SBOM 文件不应为空");
// 输出扫描结果信息
System.out.println("✅ SBOM 生成完成");
System.out.println("📁 文件位置:" + sbomFile.getAbsolutePath());
System.out.println("📏 文件大小:" + sbomFile.length() + " bytes");
boolean hasCpe = containsCpe(sbomFile);
if (!hasCpe) {
System.out.println("⚠️ 注意:SBOM 中未检测到 CPE 信息,可能需要补充 C/C++ 依赖");
} else {
System.out.println("✅ SBOM 中包含 CPE 信息");
}
}
/**
* 调用 Syft 生成 CycloneDX JSON SBOM
*/
private File generateSbomWithCpe(File scanDir, String outputFilename) throws Exception {
File outFile = new File(outputFilename);
if (!outFile.getParentFile().exists()) {
outFile.getParentFile().mkdirs();
}
List<String> command = new ArrayList<>();
command.add(SYFT_PATH);
command.add(scanDir.getAbsolutePath());
command.add("-o");
command.add("cyclonedx-json");
command.add("--file");
command.add(outFile.getAbsolutePath());
command.add("--scope");
command.add("all-layers");
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
pb.directory(scanDir.getParentFile());
System.out.println("🚀 开始 Syft 扫描...");
long start = System.currentTimeMillis();
Process proc = pb.start();
try (BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println("[syft] " + line);
}
}
int exit = proc.waitFor();
long duration = (System.currentTimeMillis() - start) / 1000;
if (exit != 0) {
throw new RuntimeException("Syft 扫描失败,exit=" + exit);
}
System.out.println("✅ Syft 扫描完成,耗时 " + duration + " 秒");
return outFile;
}
/**
* 简单校验 SBOM 是否包含 CPE
*/
private boolean containsCpe(File sbomFile) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(sbomFile))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("\"cpe\"") || line.contains("cpe:2.3")) {
return true;
}
}
}
return false;
}
}
package com.hby.shieldchaintest;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hby.shieldchaintest.entity.*;
import com.hby.shieldchaintest.mapper.CveConfigMapper;
import com.hby.shieldchaintest.mapper.CveCvssMapper;
import com.hby.shieldchaintest.mapper.CveMainMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
import java.io.*;
import java.nio.file.Files;
import java.util.*;
import java.util.stream.Collectors;
@SpringBootTest
public class VulnerabilityScannerTest {
@Autowired
private CveMainMapper cveMainMapper;
@Autowired
private CveConfigMapper cveConfigMapper;
@Autowired
private CveCvssMapper cveCvssMapper;
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
public void scanVulnerabilities() throws Exception {
PrintWriter logWriter = new PrintWriter(new FileWriter("vulnerability-scan.log"));
long startTime = System.currentTimeMillis(); // 记录开始时间
try {
logWriter.println("========== 开始漏洞扫描 ==========");
System.out.println("========== 开始漏洞扫描 ==========");
/* 1. 加载 CycloneDX SBOM */
ClassPathResource sbomResource = new ClassPathResource("mock/sbom-syft.json");
File sbomFile = sbomResource.getFile();
String sbomJson = new String(Files.readAllBytes(sbomFile.toPath()));
CycloneDxSbom sbomData = objectMapper.readValue(sbomJson, CycloneDxSbom.class);
List<SbomPackage> sbomPackages = sbomData.getComponents().stream()
.filter(c -> c.getCpe() != null && !c.getCpe().trim().isEmpty())
.map(this::convertToSbomPackage)
.collect(Collectors.toList());
logWriter.printf("📦 发现 %d 个包含 CPE 的软件包%n", sbomPackages.size());
System.out.printf("📦 发现 %d 个包含 CPE 的软件包%n", sbomPackages.size());
/* 2. 加载 CVE 数据 */
List<CveMain> allCves = cveMainMapper.findAll();
logWriter.printf("🗃️ 加载 %d 条 CVE 主记录%n", allCves.size());
System.out.printf("🗃️ 加载 %d 条 CVE 主记录%n", allCves.size());
Map<String, CveCvss> cveCvssMap = cveCvssMapper.findAll().stream()
.collect(Collectors.toMap(CveCvss::getCveId, cvss -> cvss));
List<CveConfig> allConfigs = cveConfigMapper.findAll();
logWriter.printf("⚙️ 加载 %d 条 CVE 配置%n", allConfigs.size());
System.out.printf("⚙️ 加载 %d 条 CVE 配置%n", allConfigs.size());
/* 3. 构建索引(带详细日志) */
Map<String, List<CveConfig>> cveConfigIndex = buildCveConfigIndex(allConfigs, logWriter);
int totalIndexed = cveConfigIndex.values().stream().mapToInt(List::size).sum();
logWriter.printf("🔍 CVE 配置索引完成:%d 个键,共 %d 条配置%n",
cveConfigIndex.size(), totalIndexed);
System.out.printf("🔍 CVE 配置索引完成:%d 个键,共 %d 条配置%n",
cveConfigIndex.size(), totalIndexed);
/* 4. 漏洞匹配 */
List<Vulnerability> vulnerabilities = new ArrayList<>();
for (SbomPackage pkg : sbomPackages) {
List<Vulnerability> pkgVulns = scanPackageForVulnerabilities(pkg, cveConfigIndex, allCves, cveCvssMap, logWriter);
vulnerabilities.addAll(pkgVulns);
if (!pkgVulns.isEmpty()) {
logWriter.printf("🚨 软件包【%s v%s】发现 %d 个漏洞%n",
pkg.getName(), pkg.getVersionInfo(), pkgVulns.size());
System.out.printf("🚨 软件包【%s v%s】发现 %d 个漏洞%n",
pkg.getName(), pkg.getVersionInfo(), pkgVulns.size());
}
}
/* 5. 结果汇总 */
logWriter.println("\n========== 扫描完成 ==========");
logWriter.printf("共发现 %d 个漏洞%n", vulnerabilities.size());
System.out.println("\n========== 扫描完成 ==========");
System.out.printf("共发现 %d 个漏洞%n", vulnerabilities.size());
if (!vulnerabilities.isEmpty()) {
logWriter.println("\n========== 漏洞详情 ==========");
vulnerabilities.forEach(v -> printVulnerability(v, logWriter));
}
} finally {
long endTime = System.currentTimeMillis(); // 记录结束时间
long duration = endTime - startTime; // 计算耗时
String durationFormatted = formatDuration(duration); // 格式化耗时
logWriter.printf("⏰ 扫描耗时: %s%n", durationFormatted);
System.out.printf("⏰ 扫描耗时: %s%n", durationFormatted);
logWriter.close();
System.out.println("📄 详细日志已保存至: vulnerability-scan.log");
}
}
/**
* 格式化耗时(毫秒)为可读的字符串
* @param durationMillis 耗时(毫秒)
* @return 格式化的时间字符串
*/
private String formatDuration(long durationMillis) {
long minutes = (durationMillis / 1000) / 60;
long seconds = (durationMillis / 1000) % 60;
long millis = durationMillis % 1000;
return String.format("%d分 %d秒 %d毫秒", minutes, seconds, millis);
}
/* ---------- 工具方法 ---------- */
private SbomPackage convertToSbomPackage(CycloneDxComponent component) {
SbomPackage pkg = new SbomPackage();
pkg.setName(component.getName());
pkg.setVersionInfo(component.getVersion());
ExternalRef ref = new ExternalRef();
ref.setReferenceCategory("SECURITY");
ref.setReferenceType("cpe23Type");
ref.setReferenceLocator(component.getCpe());
pkg.setExternalRefs(Collections.singletonList(ref));
return pkg;
}
private Map<String, List<CveConfig>> buildCveConfigIndex(List<CveConfig> configs, PrintWriter logWriter) {
Map<String, List<CveConfig>> index = new HashMap<>();
for (CveConfig config : configs) {
String original = config.getCpe23Uri();
String key = normalizeCpe(original);
index.computeIfAbsent(key, k -> new ArrayList<>()).add(config);
if (index.get(key).size() == 1) {
logWriter.printf("📌 索引键: %-60s ← 原始 CPE: %s%n", key, original);
}
}
return index;
}
private List<Vulnerability> scanPackageForVulnerabilities(SbomPackage pkg,
Map<String, List<CveConfig>> cveConfigIndex,
List<CveMain> allCves,
Map<String, CveCvss> cveCvssMap,
PrintWriter logWriter) {
List<Vulnerability> vulnerabilities = new ArrayList<>();
String pkgVer = pkg.getVersionInfo();
if (isInvalidVersion(pkgVer)) {
logWriter.printf("⚠️ 跳过软件包【%s】— 无效版本:%s%n", pkg.getName(), pkgVer);
return vulnerabilities;
}
logWriter.printf("%n🔍 开始扫描软件包【%s v%s】%n", pkg.getName(), pkgVer);
System.out.printf("🔍 开始扫描软件包【%s v%s】%n", pkg.getName(), pkgVer);
for (String cpeUri : pkg.getCpeUris()) {
String key = normalizeCpe(cpeUri);
logWriter.printf(" ├─ 原始 CPE: %s%n", cpeUri);
logWriter.printf(" ├─ 标准化键: %s%n", key);
List<CveConfig> matched = cveConfigIndex.getOrDefault(key, Collections.emptyList())
.stream()
.filter(c -> matchesVersion(c, pkgVer, logWriter))
.collect(Collectors.toList());
logWriter.printf(" ├─ 匹配到 %d 条配置%n", matched.size());
for (CveConfig cfg : matched) {
CveMain cve = allCves.stream()
.filter(c -> c.getCveId().equals(cfg.getCveId()))
.findFirst()
.orElse(null);
if (cve != null) {
vulnerabilities.add(new Vulnerability(pkg, cve, cveCvssMap.get(cve.getCveId()), cfg));
logWriter.printf(" ├─ ✅ 命中 CVE: %s%n", cve.getCveId());
}
}
}
return vulnerabilities;
}
private boolean matchesVersion(CveConfig cfg, String pkgVersion, PrintWriter logWriter) {
if ("*".equals(pkgVersion) || "latest".equals(pkgVersion)) {
logWriter.printf(" │ ├─ 特殊版本【%s】→ 通过%n", pkgVersion);
return true;
}
try {
ComparableVersion pv = new ComparableVersion(pkgVersion);
String start = cfg.getVersionStartIncluding();
String end = cfg.getVersionEndIncluding();
boolean okStart = start == null || pv.compareTo(new ComparableVersion(start)) >= 0;
boolean okEnd = end == null || pv.compareTo(new ComparableVersion(end)) <= 0;
boolean ok = okStart && okEnd;
logWriter.printf(" │ ├─ 版本范围:%s — %s | 包版本:%s | 结果:%s%n",
start, end, pkgVersion, ok ? "✅通过" : "❌拒绝");
return ok;
} catch (Exception e) {
logWriter.printf(" │ ├─ ⚠️ 版本解析异常:%s%n", e.getMessage());
return false;
}
}
private String normalizeCpe(String cpeUri) {
return cpeUri == null ? null : cpeUri.toLowerCase()
.replace("\\:", ":")
.replace("-", "_")
.replace(" ", "_");
}
private boolean isInvalidVersion(String version) {
return version == null || version.trim().isEmpty() || version.contains("${") || version.equals("-");
}
private void printVulnerability(Vulnerability vuln, PrintWriter writer) {
writer.println("\n----------------------------------------");
writer.println("CVE ID : " + vuln.getCveMain().getCveId());
writer.println("软件包 : " + vuln.getSbomPackage().getName() + " v" + vuln.getSbomPackage().getVersionInfo());
writer.println("匹配 CPE : " + vuln.getMatchingConfig().getCpe23Uri());
if (vuln.getCveCvss() != null) {
writer.println("严重性 : " + vuln.getCveCvss().getBaseSeverity() + " (" + vuln.getCveCvss().getBaseScore() + ")");
}
writer.println("描述 : " + StringUtils.abbreviate(vuln.getCveMain().getDescription(), 200));
}
}
用户上传软件包使用syft生成SBOM、grype生成漏洞清单功能开发
用户上传软件包 + 填写软件信息 → 后端接收 → 为软件生成DID → 将软件基本信息插入数据库获取主键作为taskId → syft 生成 SBOM → 基于 SBOM + Grype扫描漏洞 → 更新该任务状态为已完成 → 前端展示 / 导出
前端向后端提交 软件包 + 软件信息,不能等待后端返回请求结果,因为需要时间较长,会超时。
前端轮询。
- 初始化任务与存储基础信息:异步接收上传的软件压缩包和表单数据,先将软件基础信息存入数据库并获取自增主键 softwareId,再将上传文件保存到本地指定目录(以 softwareId 为标识)。
- 生成 SBOM 并更新关联信息:更新任务状态为 “SBOM 生成中”,调用方法生成 3 种格式的 SBOM 文件,随后将各 SBOM 文件的本地路径等信息更新到 software_file_info 表中,与对应 softwareId 关联。
- 漏洞扫描与报告更新:更新任务状态为 “漏洞扫描中”,基于生成的 SBOM 文件(取其中一个)通过 Grype 生成漏洞清单,再将漏洞报告的本地路径等信息更新到对应 software_file_info 记录中。
- 任务结果收尾处理:若流程无异常,更新任务状态为 “成功完成”;若执行过程中出现异常,记录错误日志,若已生成 softwareId 则将任务状态更新为 “失败”,并抛出异常提示任务处理失败,最终返回 softwareId 作为任务 ID 供查询结果。
由于原先的题目要求爬虫得到CVE的漏洞信息,存储到本地数据库中,使用本地漏洞库为用户生成漏洞清单,现在自己开发的话简化实现:不再使用本地漏洞库且基于这个实现漏洞扫描,直接使用开源工具+他们的更完整的漏洞库。
前端开发
前端API
/**
* 提交软件包+软件信息,启动一体化任务(生成SBOM+漏洞清单)
*/
export const submitSoftware = (formData) => {
return request({
url: '/sbom-vuln/submit',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/**
* 轮询查询任务结果
*/
export const queryTaskResult = (taskId) => {
return request({
url: `/sbom-vuln/query/${taskId}`,
method: 'get'
})
}
2. 前端逻辑
// 导入新API
import { submitSoftware, queryTaskResult } from '@/api/software.js'
// 新增:任务ID(轮询用)
const taskId = ref(null)
// 新增:任务状态(用于显示loading/结果)
const taskStatus = ref('idle') // idle/processing/success/fail
// 新增:漏洞清单结果
const vulnResult = ref(null)
// 处理本地文件上传(保持不变,仅保存文件)
const handleLocalUpload = async (file) => {
currentFile.value = file.raw // 保存原始文件对象
}
修改这个表,和后端数据库设计差别太大

const softwareInfo = ref({
name: '',
softwareType: '',
version: '',
dependRate: '',
language: '',
supplier: '',
licensePeriod: '',
licenseType: '',
remarks: ''
})
package com.sky.dto;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(description = "用户上传软件包填写的软件信息使用的数据模型")
//这个DTO只接收表单数据,文件在controller层单独接收
public class SoftwareUploadInfoDTO implements Serializable {
private String name;
// 字符串 0-开源软件,1-商业软件
private String softwareType;
private String version;
private String dependRate; //依赖率
private String markerInfo;
private String language;//软件使用语言
private String licensePeriod;
private String licenseType;
private String remarks;
}
后端开发
1. 用户提交的表单SoftwareUploadInfoDTO
package com.sky.dto;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(description = "用户上传软件包填写的软件信息使用的数据模型")
//这个DTO只接收表单数据,文件在controller层单独接收
public class SoftwareUploadInfoDTO implements Serializable {
private String name;
private String integrity;
private String version;
private String componentsCount;
private String markerInfo;
private String supplier;
private String licensePeriod;
private String licenseType;
private String remarks;
}
2. 实体SoftwareFileInfo
package com.sky.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SoftwareFileInfo implements Serializable {
// 序列化机制的版本标识,用于反序列化时验证类的版本一致性(如果类结构发生变化,修改此值可以避免反序列化异常)
private static final long serialVersionUID = 1L;
private Integer id;
private Integer softwareId;
// SBOM文件本地存储路径(如:C:/Users/xxx/SoftwareFiles/123_sbom.json)
private String sbomLocalPath;
private String sbomFormat;
private String vulnListLocalPath;
private String vulnListFormat;
// 任务处理状态:0-处理中,1-SBOM成功生成,2-漏洞清单成功生成
private Integer taskStatus;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer createUser;
private Integer updateUser;
}
3. 后端SbomVulnController
前端提交软件包 + 表单数据,后端先保存软件包到本地,并插入数据库,然后生成DID、公私钥对,向数据库插入相关信息;生成三种格式SBOM,更新数据库;接着分别生成三个漏洞清单,更新数据库。
package com.sky.controller;
import com.sky.dto.SoftwareUploadInfoDTO;
import com.sky.result.Result;
import com.sky.service.SbomVulnService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/sbom-vuln")
@Slf4j
@Api(tags = "生成SBOM和漏洞清单接口")
public class SbomVulnController {
@Autowired
private SbomVulnService sbomVulnService;
/**
* 基于用户上传的软件包+软件信息,生成SBOM和漏洞清单
* @return
*/
@PostMapping("/submit")
@ApiOperation(value = "基于用户上传的软件包+软件信息,生成SBOM和漏洞清单")
public Result<String> submitSoftware(
@ApiParam(value = "软件压缩包文件", required = true)
@RequestParam("file") MultipartFile file,
@ApiParam(value = "软件信息表单数据", required = true)
SoftwareUploadInfoDTO info
){
try {
// 1. 日志打印接收的参数(便于调试)
log.info("接收软件处理任务:文件名={}, 软件名称={}, SBOM格式={}, 报告格式={}",
file.getOriginalFilename(), info.getName());
// 2. 调用Service层提交任务(生成taskId,异步处理)
String taskId = sbomVulnService.submitSoftwareTask(file, info);
// 3. 返回任务ID给前端(用于轮询结果)
return Result.success(taskId);
} catch (Exception e) {
log.error("提交软件处理任务失败", e);
return Result.error("提交失败:" + e.getMessage());
}
}
/**
* 轮询任务结果(根据taskId查询处理状态和结果)
*
* @param taskId 任务ID
* @return 任务状态(processing/success/fail)+ 结果数据(成功时返回SBOM和漏洞信息)
*/
@PostMapping("/query-result")
@ApiOperation(value = "根据taskId查询处理状态和结果")
public Result<?> queryTaskResult(
@ApiParam(value = "任务ID", required = true)
@RequestParam("taskId") String taskId
) {
try {
// 调用Service查询任务结果
Object result = sbomVulnService.queryTaskResult(taskId);
return Result.success(result);
} catch (Exception e) {
log.error("查询任务结果失败", e);
return Result.error("查询失败:" + e.getMessage());
}
}
}
4. 后端SbomVulnService
package com.sky.service;
import com.sky.dto.SoftwareUploadInfoDTO;
import org.springframework.web.multipart.MultipartFile;
public interface SbomVulnService {
/**
* 提交软件处理任务(生成SBOM+漏洞扫描)
* @param file 上传的软件压缩包
* @param info 软件信息表单数据
* @return 任务ID(用于查询结果)
*/
String submitSoftwareTask(MultipartFile file, SoftwareUploadInfoDTO info);
/**
* 查询任务处理结果
* @param taskId 任务ID
* @return 任务结果状态
*/
Object queryTaskResult(String taskId);
}
5. serviceImpl
package com.sky.service.impl;
import com.sky.dto.SoftwareUploadInfoDTO;
import com.sky.entity.Software;
import com.sky.entity.SoftwareFileInfo;
import com.sky.exception.DatabaseException;
import com.sky.mapper.SbomVulnMapper;
import com.sky.mapper.SoftwareMapper;
import com.sky.properties.SyftProperties;
import com.sky.service.SbomVulnService;
import com.sky.utils.DIDUtil;
import com.sky.utils.SimpleEncryptUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.time.LocalDateTime;
import java.util.*;
/**
* SBOM和漏洞扫描服务
*/
@Service
@Slf4j
public class SbomVulnServiceImpl implements SbomVulnService {
//@Autowired
//private SyftProperties syftProperties;
@Autowired
private SoftwareMapper softwareMapper;
/** 本地 syft 可执行文件路径 */
private final String SYFT_PATH = "D:\\syft\\syft.exe";
private final String GRYPE_PATH = "D:\\grype\\grype.exe";
// 本地存储路径 - 替换OSS
private static final String SOFTWARE_STORAGE = "E:\\shieldchain\\software-storage";
private static final String SBOM_STORAGE = "E:\\shieldchain\\sbom-storage";
private static final String VULN_STORAGE = "E:\\shieldchain\\vuln-storage";
// 三种固定输出格式
private static final String[][] OUTPUT_FORMATS = {
{"cyclonedx-json", "cdx.json"},
{"spdx-json", "spdx.json"},
{"syft-json", "syft.json"}
};
// 漏洞报告格式
private static final String VULN_REPORT_FORMAT = "json";
/**
* 提交软件处理任务(生成固定格式SBOM+漏洞扫描)
* @param file 上传的软件压缩包
* @param softwareUploadInfoDTO 软件信息表单数据
* @return 任务ID(用于查询结果)
*/
// 使用线程池异步执行,不要阻塞进程,taskExecutor是线程池名称
@Async("taskExecutor")
public String submitSoftwareTask(MultipartFile file, SoftwareUploadInfoDTO softwareUploadInfoDTO) {
log.info("开始处理软件任务: 文件名={}", file.getOriginalFilename());
Integer softwareId = null;
try {
// 1. 创建并保存 Software 实体到数据库,获取自增主键ID
softwareId = saveSoftwareToDatabase(softwareUploadInfoDTO);
log.info("软件信息已保存到数据库,生成主键ID: {}", softwareId);
// 2. 使用主键ID作为标识保存上传文件,得到文件的绝对地址
File savedFile = saveUploadedFile(file, softwareId.toString());
// 3. 生成3种格式的SBOM
updateTaskStatus(softwareId, 1); // 1-SBOM生成中
List<File> sbomFiles = generateMultipleSbomFormats(savedFile, softwareId.toString());
// 4. 更新数据库software_file_info表中的SBOM文件路径信息
updateSoftwareWithSbomInfo(softwareId, sbomFiles);
// 5. 基于生成的其中一个SBOM使用grype生成漏洞清单
updateTaskStatus(softwareId, 2); // 2-漏洞扫描中
List<File> vulnReports = performVulnerabilityScanning(sbomFiles, softwareId.toString()); // 调用新函数
updateSoftwareWithVulnInfo(softwareId, vulnReports); // 更新漏洞报告信息
// 6. 更新任务状态为成功,更新漏洞清单文件路径信息
// 只更新software表,因为software_file_info表状态为2就表示已全部生成成功
updateTaskStatus(softwareId, 3); // 3-成功完成
log.info("软件任务处理完成: softwareId={}, 生成{}个SBOM文件, {}个漏洞报告",
softwareId, sbomFiles.size(), vulnReports.size());
return softwareId.toString();
} catch (Exception e) {
log.error("软件任务处理失败: softwareId={}, 错误: {}", softwareId, e.getMessage(), e);
// 如果已经生成了ID,更新任务状态为失败
if (softwareId != null) {
updateTaskStatus(softwareId, 4); // 4-失败
}
throw new RuntimeException("任务处理失败: " + e.getMessage(), e);
}
}
/**
* 保存软件信息到数据库并返回自增主键
*/
private Integer saveSoftwareToDatabase(SoftwareUploadInfoDTO softwareUploadInfoDTO) {
try {
// 生成DID和密钥对
KeyPair keyPair = DIDUtil.generateECCKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// 生成软件DID
String fullDid = DIDUtil.generateSoftwareDID(publicKey);
// 加密私钥
String encryptedPrivateKey = SimpleEncryptUtil.encryptForSoftware(
Base64.getEncoder().encodeToString(privateKey.getEncoded())
);
// 创建Software实体
Software software = Software.builder()
.did(fullDid)
.name(softwareUploadInfoDTO.getName())
.dependRate(softwareUploadInfoDTO.getDependRate())
.version(softwareUploadInfoDTO.getVersion())
.softwareType(Integer.valueOf(softwareUploadInfoDTO.getSoftwareType()))//转成数字
.language(softwareUploadInfoDTO.getLanguage())
.publicKey(Base64.getEncoder().encodeToString(publicKey.getEncoded()))
.privateKeyEncrypt(encryptedPrivateKey)
.taskStatus(0) // 0-处理中
.didStatus(1) // 1-有效
.keyRotationCycle(30)
.lastRotationTime(LocalDateTime.now())
.build();
// 插入数据库,获取自增主键
int result = softwareMapper.insertSoftware(software);
if (result <= 0) {
throw new DatabaseException("保存软件信息到数据库software表失败");
}
// 返回自增主键ID
return software.getId();
} catch (Exception e) {
log.error("保存软件信息到数据库software表失败", e);
throw new RuntimeException("数据库操作失败: " + e.getMessage(), e);
}
}
/**
* 保存上传的文件到本地存储目录
*/
private File saveUploadedFile(MultipartFile file, String taskId) throws IOException {
// 创建任务目录
File taskDir = new File(SOFTWARE_STORAGE, taskId);
if (!taskDir.exists()) {
taskDir.mkdirs();
}
// 确保存储根目录存在
File storageDir = new File(SOFTWARE_STORAGE);
if (!storageDir.exists()) {
storageDir.mkdirs();
log.info("创建软件存储目录: {}", storageDir.getAbsolutePath());
}
String originalFilename = file.getOriginalFilename();
File savedFile = new File(taskDir, originalFilename);
file.transferTo(savedFile);
log.info("文件保存到本地成功: {} -> {}", originalFilename, savedFile.getAbsolutePath());
return savedFile; //返回文件
}
/**
* 生成三种格式的SBOM
*/
private List<File> generateMultipleSbomFormats(File scanFile, String taskId) {
List<File> sbomFiles = new ArrayList<>();
// 创建SBOM存储目录
File sbomTaskDir = new File(SBOM_STORAGE, taskId);
if (!sbomTaskDir.exists()) {
sbomTaskDir.mkdirs();
}
// 确保SBOM存储根目录存在
File sbomStorageDir = new File(SBOM_STORAGE);
if (!sbomStorageDir.exists()) {
sbomStorageDir.mkdirs();
log.info("创建SBOM存储目录: {}", sbomStorageDir.getAbsolutePath());
}
try {
// 构建Syft命令 - 同时生成三种格式[1,3](@ref)
List<String> command = new ArrayList<>();
command.add(SYFT_PATH);
command.add(scanFile.getAbsolutePath());
command.add("--scope");
command.add("all-layers");
// 添加三种输出格式[1](@ref)
for (String[] format : OUTPUT_FORMATS) {
String outputFormat = format[0];
String outputFilename = "sbom-" + taskId + "." + format[1];
File outputFile = new File(sbomTaskDir, outputFilename);
command.add("-o");
command.add(outputFormat + "=" + outputFile.getAbsolutePath());
}
// 添加详细输出选项
command.add("-v");
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
pb.directory(scanFile.getParentFile());
long startTime = System.currentTimeMillis();
log.info("开始Syft多格式扫描: 目标={}, 生成{}种格式",
scanFile.getName(), OUTPUT_FORMATS.length);
Process process = pb.start();
// 读取输出日志
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
log.debug("[Syft] {}", line);
}
}
int exitCode = process.waitFor();
long duration = System.currentTimeMillis() - startTime;
if (exitCode != 0) {
throw new RuntimeException("Syft执行失败,退出码: " + exitCode + "\n输出: " + output);
}
// 检查所有SBOM文件是否生成成功
for (String[] format : OUTPUT_FORMATS) {
String outputFilename = "sbom-" + taskId + "." + format[1];
File outputFile = new File(sbomTaskDir, outputFilename);
if (!outputFile.exists() || outputFile.length() == 0) {
throw new RuntimeException("SBOM文件生成失败: " + outputFilename);
}
sbomFiles.add(outputFile);
log.info("SBOM文件生成成功: {} (大小: {} bytes)",
outputFilename, outputFile.length());
}
log.info("Syft多格式扫描完成: 耗时{}ms, 生成{}个文件",
duration, sbomFiles.size());
return sbomFiles;
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Syft执行异常: " + e.getMessage(), e);
}
}
/**
* 使用Grype进行漏洞扫描
*/
private List<File> performVulnerabilityScanning(List<File> sbomFiles, String softwareId) {
List<File> vulnReports = new ArrayList<>();
// 创建漏洞报告存储目录
File vulnSoftwareDir = new File(VULN_STORAGE, softwareId);
if (!vulnSoftwareDir.exists() && !vulnSoftwareDir.mkdirs()) {
throw new RuntimeException("创建漏洞报告存储目录失败: " + vulnSoftwareDir.getAbsolutePath());
}
for (File sbomFile : sbomFiles) {
try {
// 根据SBOM文件名生成对应的漏洞报告文件名
String sbomFilename = sbomFile.getName();
String vulnReportFilename = "vuln-report_" + sbomFilename.replace("sbom.", "");
File vulnReportFile = new File(vulnSoftwareDir, vulnReportFilename);
// 构建Grype命令
List<String> command = Arrays.asList(
GRYPE_PATH,
"sbom:" + sbomFile.getAbsolutePath(),
"-o", VULN_REPORT_FORMAT,
"--file", vulnReportFile.getAbsolutePath()
);
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
long startTime = System.currentTimeMillis();
log.info("开始漏洞扫描: SBOM文件={}", sbomFilename);
Process process = pb.start();
// 读取输出日志
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
log.debug("[Grype] {}", line);
}
}
// 添加超时控制
boolean finished = process.waitFor(3, java.util.concurrent.TimeUnit.MINUTES);
int exitCode = -1;
long duration = System.currentTimeMillis() - startTime;
if (!finished) {
process.destroyForcibly();
throw new RuntimeException("漏洞扫描超时,已终止进程,耗时: " + duration + "ms");
}
exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Grype执行失败,退出码: " + exitCode + "\n输出: " + output);
}
if (!vulnReportFile.exists() || vulnReportFile.length() == 0) {
throw new RuntimeException("漏洞报告生成失败: " + vulnReportFilename);
}
vulnReports.add(vulnReportFile);
log.info("漏洞报告生成成功: {} (大小: {} bytes), 耗时{}ms",
vulnReportFilename, vulnReportFile.length(), duration);
} catch (Exception e) {
log.error("漏洞扫描失败: SBOM文件={}", sbomFile.getName(), e);
// 继续处理其他SBOM文件,不立即失败
if (sbomFiles.size() == 1) {
throw new RuntimeException("漏洞扫描失败: " + e.getMessage(), e);
}
}
}
if (vulnReports.isEmpty()) {
throw new RuntimeException("所有漏洞扫描尝试均失败");
}
return vulnReports;
}
/**
* 从文件名提取格式信息
*/
private String getFormatFromFilename(String filename) {
if (filename.contains("cdx")) return "cyclonedx-json";
if (filename.contains("spdx")) return "spdx-json";
if (filename.contains("syft")) return "syft-json";
return "unknown";
}
/**
* 更新软件SBOM文件信息到数据库
*/
private void updateSoftwareWithSbomInfo(Integer softwareId, List<File> sbomFiles) {
try {
for (File sbomFile : sbomFiles) {
SoftwareFileInfo fileInfo = SoftwareFileInfo.builder()
.softwareId(softwareId)
.sbomLocalPath(sbomFile.getAbsolutePath())
.sbomFormat(getFormatFromFilename(sbomFile.getName()))
.taskStatus(1) // 1-sbom生成成功
.build();
int result = softwareMapper.insertFileInfo(fileInfo);
if (result <= 0) {
log.warn("保存SBOM文件信息到数据库失败: softwareId={}, file={}",
softwareId, sbomFile.getName());
} else {
log.info("SBOM文件信息保存成功: softwareId={}, file={}",
softwareId, sbomFile.getName());
}
}
} catch (Exception e) {
log.error("更新SBOM文件信息到数据库失败: softwareId={}", softwareId, e);
// 不抛出异常,避免影响主流程
}
}
/**
* 更新漏洞报告信息到数据库
*/
private void updateSoftwareWithVulnInfo(Integer softwareId, List<File> vulnReports) {
try {
for (File vulnReport : vulnReports) {
// 查找对应的SBOM记录
SoftwareFileInfo fileInfo = softwareMapper.selectBySoftwareIdAndFormat(
softwareId, getFormatFromFilename(vulnReport.getName().replace("vuln-report_", ""))
);
if (fileInfo != null) {
fileInfo.setVulnListLocalPath(vulnReport.getAbsolutePath());
fileInfo.setVulnListFormat(VULN_REPORT_FORMAT);
fileInfo.setTaskStatus(2); //2-成功生成漏洞清单
int result = softwareMapper.updateFileInfo(fileInfo);
if (result > 0) {
log.info("漏洞报告信息更新成功: softwareId={}, file={}",
softwareId, vulnReport.getName());
}
}
}
} catch (Exception e) {
log.error("更新漏洞报告信息到数据库失败: softwareId={}", softwareId, e);
// 不抛出异常,避免影响主流程
}
}
/**
* 更新software表任务状态
*/
private void updateTaskStatus(Integer softwareId, Integer status) {
try {
Software software = new Software();
software.setId(softwareId);
software.setTaskStatus(status);
int result = softwareMapper.updateTaskStatus(software);
if (result > 0) {
log.debug("任务状态更新成功: softwareId={}, status={}", softwareId, status);
}
} catch (Exception e) {
log.error("更新任务状态失败: softwareId={}, status={}", softwareId, status, e);
}
}
/**
* 查询任务结果
*/
@Override
public Object queryTaskResult(String taskId) {
try {
Integer softwareId = Integer.parseInt(taskId);
// 查询软件基本信息
Software software = softwareMapper.selectById(softwareId);
if (software == null) {
return Map.of("status", "not_found", "message", "任务不存在");
}
Map<String, Object> result = new HashMap<>();
result.put("softwareId", softwareId);
result.put("softwareName", software.getName());
result.put("version", software.getVersion());
result.put("taskStatus", software.getTaskStatus());
result.put("did", software.getDid());
// 根据任务状态返回相应信息
switch (software.getTaskStatus()) {
case 0: // 处理中
result.put("status", "processing");
result.put("message", "任务处理中");
break;
case 1: // SBOM生成中
result.put("status", "processing");
result.put("message", "SBOM生成中");
break;
case 2: // 漏洞扫描中
result.put("status", "processing");
result.put("message", "漏洞扫描中");
break;
case 3: // 成功完成
// 查询SBOM和漏洞报告文件信息
List<SoftwareFileInfo> fileInfos = softwareMapper.selectFileInfosBySoftwareId(softwareId);
List<Map<String, Object>> sbomFiles = new ArrayList<>();
List<Map<String, Object>> vulnReports = new ArrayList<>();
for (SoftwareFileInfo fileInfo : fileInfos) {
if (fileInfo.getSbomLocalPath() != null) {
sbomFiles.add(Map.of(
"format", fileInfo.getSbomFormat(),
"path", fileInfo.getSbomLocalPath(),
"filename", new File(fileInfo.getSbomLocalPath()).getName()
));
}
if (fileInfo.getVulnListLocalPath() != null) {
vulnReports.add(Map.of(
"format", fileInfo.getVulnListFormat(),
"path", fileInfo.getVulnListLocalPath(),
"filename", new File(fileInfo.getVulnListLocalPath()).getName()
));
}
}
result.put("status", "completed");
result.put("sbomFiles", sbomFiles);
result.put("vulnReports", vulnReports);
result.put("message", String.format(
"任务处理完成,生成%d个SBOM文件,%d个漏洞报告",
sbomFiles.size(), vulnReports.size()
));
break;
case 4: // 失败
result.put("status", "failed");
result.put("message", "任务处理失败");
break;
default:
result.put("status", "unknown");
result.put("message", "任务状态未知");
}
return result;
} catch (Exception e) {
log.error("查询任务结果失败: taskId={}", taskId, e);
return Map.of("status", "error", "message", "查询失败: " + e.getMessage());
}
}
/**
* 获取SBOM文件内容
*/
public String getSbomContent(String taskId, String format) {
try {
for (String[] outputFormat : OUTPUT_FORMATS) {
if (outputFormat[0].equals(format)) {
String filename = "sbom-" + taskId + "." + outputFormat[1];
File sbomFile = new File(SBOM_STORAGE, taskId + File.separator + filename);
if (sbomFile.exists()) {
return new String(Files.readAllBytes(sbomFile.toPath()));
}
}
}
throw new RuntimeException("未找到指定格式的SBOM文件: " + format);
} catch (IOException e) {
throw new RuntimeException("读取SBOM文件失败: " + e.getMessage(), e);
}
}
}
6. softwareMapper
/**
* 插入software并返回主键
* @param software
* @return
*/
@AutoFill(value = OperationType.INSERT)
int insertSoftware(Software software);
/**
* 插入SBOM文件信息
* @param fileInfo SBOM文件信息实体
* @return 影响行数(1成功,0失败)
*/
@AutoFill(value = OperationType.INSERT)
int insertFileInfo(SoftwareFileInfo fileInfo);
/**
* 根据软件ID和SBOM格式查询对应的文件信息记录
* @param softwareId
* @param formatFromFilename
* @return
*/
@Select("select * from software_file_info where software_id = #{softwareId} and sbom_format = #{formatFromFilename}")
SoftwareFileInfo selectBySoftwareIdAndFormat(Integer softwareId, String formatFromFilename);
/**
* 更新SBOM关联的漏洞报告信息
* @param fileInfo 包含更新字段的SoftwareFileInfo实体(需携带id主键)
* @return 影响行数(1成功,0无匹配记录)
*/
@AutoFill(value = OperationType.UPDATE)
int updateFileInfo(SoftwareFileInfo fileInfo);
/**
* 更新software表的任务状态
* @param software 包含id(主键)和taskStatus(目标状态)的实体
* @return 影响行数(1成功,0无匹配记录)
*/
@AutoFill(value = OperationType.UPDATE)
@Update("update software set task_status = #{taskStatus}, update_time = #{updateTime}, update_user = #{updateUser} " +
"where id = #{id}")
int updateTaskStatus(Software software);
/**
* 根据id查询software
* @param softwareId
* @return
*/
@Select("select * from software where id = #{id}")
Software selectById(Integer softwareId);
/**
* 根据softwareId查询所有software_file_info
* @param softwareId
* @return
*/
@Select("select * from software_file_info where software_id = #{softwareId}")
List<SoftwareFileInfo> selectFileInfosBySoftwareId(Integer softwareId);
<insert id="insertSoftware" parameterType="com.sky.entity.Software" useGeneratedKeys="true" keyProperty="id">
INSERT INTO software
<trim prefix="(" suffix=")" suffixOverrides=",">
<!-- 动态拼接非空字段 -->
<if test="did != null and did != ''">did,</if>
<if test="name != null and name != ''">name,</if>
<if test="dependRate != null and dependRate != ''">depend_rate,</if>
<if test="version != null and version != ''">version,</if>
<if test="softwareType != null">software_type,</if> <!-- Integer类型只需判断非null -->
<if test="teamId != null">team_id,</if>
<if test="language != null and language != ''">language,</if>
<if test="didStatus != null">did_status,</if>
<if test="taskStatus != null">task_status,</if>
<if test="createTime != null">create_time,</if> <!-- 数据库字段若为下划线命名需对应 -->
<if test="updateTime != null">update_time,</if>
<if test="createUser != null">create_user,</if>
<if test="updateUser != null">update_user,</if>
<if test="publicKey != null and publicKey != ''">public_key,</if>
<if test="privateKeyEncrypt != null and privateKeyEncrypt != ''">private_key_encrypt,</if>
<if test="keyRotationCycle != null">key_rotation_cycle,</if>
<if test="lastRotationTime != null">last_rotation_time,</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<!-- 对应字段的参数值 -->
<if test="did != null and did != ''">#{did},</if>
<if test="name != null and name != ''">#{name},</if>
<if test="dependRate != null and dependRate != ''">#{dependRate},</if>
<if test="version != null and version != ''">#{version},</if>
<if test="softwareType != null">#{softwareType},</if>
<if test="teamId != null">#{teamId},</if>
<if test="language != null and language != ''">#{language},</if>
<if test="didStatus != null">#{didStatus},</if>
<if test="taskStatus != null">#{taskStatus},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="createUser != null">#{createUser},</if>
<if test="updateUser != null">#{updateUser},</if>
<if test="publicKey != null and publicKey != ''">#{publicKey},</if>
<if test="privateKeyEncrypt != null and privateKeyEncrypt != ''">#{privateKeyEncrypt},</if>
<if test="keyRotationCycle != null">#{keyRotationCycle},</if>
<if test="lastRotationTime != null">#{lastRotationTime},</if>
</trim>
</insert>
<!-- 插入SBOM文件信息 -->
<insert id="insertFileInfo" parameterType="com.sky.entity.SoftwareFileInfo" useGeneratedKeys="true" keyProperty="id">
INSERT INTO software_file_info
<trim prefix="(" suffix=")" suffixOverrides=",">
software_id,
sbom_local_path,
sbom_format,
task_status,
<!-- 动态字段:直接使用属性名 -->
<if test="vulnListLocalPath != null and vulnListLocalPath != ''">vuln_list_local_path,</if>
<if test="vulnListFormat != null and vulnListFormat != ''">vuln_list_format,</if>
<!-- 公共字段 -->
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
<if test="createUser != null">create_user,</if>
<if test="updateUser != null">update_user,</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<!-- 必传字段值:直接使用属性名 -->
#{softwareId},
#{sbomLocalPath},
#{sbomFormat},
#{taskStatus},
<!-- 动态字段值 -->
<if test="vulnListLocalPath != null and vulnListLocalPath != ''">#{vulnListLocalPath},</if>
<if test="vulnListFormat != null and vulnListFormat != ''">#{vulnListFormat},</if>
<!-- 公共字段值 -->
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="createUser != null">#{createUser},</if>
<if test="updateUser != null">#{updateUser},</if>
</trim>
</insert>
<!-- 更新漏洞报告信息到SoftwareFileInfo -->
<update id="updateFileInfo" parameterType="com.sky.entity.SoftwareFileInfo">
UPDATE software_file_info
<set>
<!-- 漏洞报告本地路径(非空才更新) -->
<if test="vulnListLocalPath != null and vulnListLocalPath != ''">
vuln_list_local_path = #{vulnListLocalPath},
</if>
<!-- 漏洞报告格式(非空才更新) -->
<if test="vulnListFormat != null and vulnListFormat != ''">
vuln_list_format = #{vulnListFormat},
</if>
<!-- 任务状态(若有更新需求,非空才更新) -->
<if test="taskStatus != null">
task_status = #{taskStatus},
</if>
<!-- 更新时间(每次更新必改,无需判断) -->
update_time = #{updateTime},
update_user = #{updateUser},
</set>
<!-- 必须通过主键id定位记录,确保更新准确性 -->
WHERE id = #{id}
</update>
功能调试
问题是:
根据错误日志,您的应用抛出了 MaxUploadSizeExceededException,具体原因是文件大小超过了1048576字节(1MB)的限制
解决方案:在application.yml配置
spring:
servlet:
multipart:
enabled: true
max-file-size: 50MB
max-request-size: 100MB







线程池
使用线程池:
@Async:Spring 提供的注解,用于声明方法需要 “异步执行”。即调用该方法时,主线程不会等待方法执行完成,而是直接返回,方法的逻辑会在后台线程中单独运行。"taskExecutor":指定异步方法使用的线程池名称。这里表示该方法会提交到名为taskExecutor的线程池中执行,由线程池管理线程资源(避免频繁创建 / 销毁线程,提高性能)。
由于submitSoftwareTask 方法包含耗时操作:
- 解压上传的软件压缩包(可能很大,耗时几秒到几分钟);
- 调用
syft生成 SBOM(扫描大目录时耗时较长); - 后续可能的漏洞扫描等。
如果不使用 @Async,这些操作会阻塞主线程(比如 Web 请求线程),导致前端长时间等待响应(甚至超时)。而加上 @Async("taskExecutor") 后:
- 主线程调用该方法时,会立即生成
taskId并返回给前端(用户可以用这个 ID 后续查询结果); - 解压、生成 SBOM 等耗时逻辑在后台线程池中异步执行,不影响主线程处理其他请求。
CPE是什么?
CPE 是 Common Platform Enumeration(通用平台枚举) 的缩写,本质是一种标准化的组件识别格式,用于唯一标识软件、硬件或固件等 IT 组件。
核心作用
CPE 的核心价值是 “统一命名规则”—— 不同工具、不同系统之间,通过 CPE 能精准识别同一个组件,避免因命名混乱导致的识别错误(比如 “Chrome” 和 “谷歌浏览器” 本质是同一个软件,CPE 能统一标识)。
具体格式(以 CPE 2.3 版本为例)
标准格式为分层字符串,用冒号分隔,结构如下:
plaintext
cpe:2.3:<部分>:<厂商>:<产品>:<版本>:<更新>:<编辑版本>:<语言>:<软件版本>:<目标硬件>:<目标软件>:<其他>
为什么对你的 SBOM 生成很重要?
结合你用 Syft 生成 SBOM 的场景,CPE 有两个核心作用:
- 漏洞匹配:漏洞数据库(如 NVD)中的漏洞都是关联 CPE 的,有了 CPE,才能快速通过 SBOM 定位 “哪些组件存在已知漏洞”(比如用 Grype 扫描 SBOM 找漏洞,必须依赖 CPE);
- 合规检查:不同组件的许可证、合规要求可通过 CPE 关联查询,避免使用不合规的组件。
这也是之前代码中会专门校验 SBOM 是否包含 CPE 的原因 —— 缺少 CPE 的 SBOM,后续漏洞扫描、合规检查都会受影响。
安装syft
在Windows系统上将Syft下载安装到D盘很简单,主要通过直接下载预编译的二进制文件来完成。下面是为您整理的详细步骤和后续使用方法。
📥 下载Syft二进制文件
这是最推荐的方法,直接获取可执行文件。
-
访问发布页面:打开 Syft 的官方 GitHub 发布页面:
https://github.com/anchore/syft/releases。 -
选择Windows版本:在最新的发布版本(例如
vx.x.x)下,找到适用于 Windows 的压缩包。它的名称通常类似于syft_x.x.x_windows_amd64.zip。 -
下载到D盘:点击这个链接开始下载。在保存时,请直接选择保存到D盘的目标文件夹,比如 `D:\Tools`。浏览器会自动将文件下载到你指定的位置。
📁 安装与配置
下载完成后,进行解压和配置。
-
解压文件:找到刚刚下载的
.zip文件,右键点击它,选择“全部提取”或使用解压软件(如WinRAR、7-Zip)解压。将解压目标也设置为D盘的同一个文件夹,例如D:\Tools\`。解压后,你会得到一个名为syft.exe` 的可执行文件。 -
配置环境变量(关键步骤):为了能在任何路径下直接运行
syft命令,需要将它的所在目录添加到系统的 PATH 环境变量中。-
在任务栏的搜索框里输入“编辑系统环境变量”并打开它。
-
在弹出的窗口中,点击下方的“环境变量”按钮。
-
在“系统变量”区域,找到并选中名为
Path的变量,然后点击“编辑”。 -
在新弹出的窗口中,点击“新建”,然后将Syft.exe所在的完整路径(例如
D:\Tools)添加进去。 -
依次点击“确定”保存所有打开的窗口。
-
✅ 验证安装
配置完成后,需要检查一下是否成功。
-
按
Win + R键,输入cmd然后按回车,打开命令提示符。 -
在新打开的窗口中,直接输入以下命令并按回车:

🔧 基本使用命令
安装成功后,你就可以使用Syft来生成SBOM了。这里有一些基本命令示例:
-
扫描一个目录:如果你想分析D盘上的一个软件项目,可以使用以下命令(将路径替换为你的实际路径):
syft dir:D:\your-project-path -
指定输出格式和文件:使用
-o参数指定SBOM格式(如cyclonedx-json),并用--file参数将结果保存到文件:✅ 正确的命令格式应该是: syft D:\下载\syft_1.37.0_windows_amd64.zip --output spdx-json=D:\sbom.json syft <扫描目标> --output <格式>=<输出路径> -
扫描Docker镜像:你也可以直接扫描Docker镜像:
syft your-image:tag
测试syft生成SBOM
只能用cmd.exe管理员模式打开,不能用powershell


同理安装Grype

将漏洞库的拉取位置改成自定义

|
主要用途 |
命令格式 |
关键参数/选项说明 |
使用示例 |
|---|---|---|---|
|
扫描容器镜像 |
|
支持 Docker Hub、私有仓库、本地镜像存档 ( |
|
|
扫描文件系统/目录 |
|
扫描项目目录或解压后的文件系统,如检查 |
|
|
扫描 SBOM 文件 |
|
直接分析由 Syft 等工具生成的软件物料清单文件,效率很高。 |
|
|
控制输出与行为 |
|
|
|
扫描E:\shieldchain\sbom-storage\sbom.json,并将漏洞报告存储到E:\shieldchain\software-storage,怎么写命令
grype sbom:E:\shieldchain\sbom-storage\sbom.json -o json > E:\shieldchain\software-storage\vulnerability-report.json

TODO:前端提交之后,就在“项目管理”部分新建一个项目,状态显示轮询的状态,我觉得后端不需要传给前端生成结果,前端在“项目管理”部分点击这个项目的“查看详情”,前端向后端发送查询请求,从后端数据库读取再返回给前端
调整逻辑
更多推荐

所有评论(0)