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扫描漏洞 → 更新该任务状态为已完成 → 前端展示 / 导出

前端向后端提交 软件包 + 软件信息,不能等待后端返回请求结果,因为需要时间较长,会超时。

前端轮询。

  1. 初始化任务与存储基础信息:异步接收上传的软件压缩包和表单数据,先将软件基础信息存入数据库并获取自增主键 softwareId,再将上传文件保存到本地指定目录(以 softwareId 为标识)。
  2. 生成 SBOM 并更新关联信息:更新任务状态为 “SBOM 生成中”,调用方法生成 3 种格式的 SBOM 文件,随后将各 SBOM 文件的本地路径等信息更新到 software_file_info 表中,与对应 softwareId 关联。
  3. 漏洞扫描与报告更新:更新任务状态为 “漏洞扫描中”,基于生成的 SBOM 文件(取其中一个)通过 Grype 生成漏洞清单,再将漏洞报告的本地路径等信息更新到对应 software_file_info 记录中。
  4. 任务结果收尾处理:若流程无异常,更新任务状态为 “成功完成”;若执行过程中出现异常,记录错误日志,若已生成 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 有两个核心作用:

  1. 漏洞匹配:漏洞数据库(如 NVD)中的漏洞都是关联 CPE 的,有了 CPE,才能快速通过 SBOM 定位 “哪些组件存在已知漏洞”(比如用 Grype 扫描 SBOM 找漏洞,必须依赖 CPE);
  2. 合规检查:不同组件的许可证、合规要求可通过 CPE 关联查询,避免使用不合规的组件。

这也是之前代码中会专门校验 SBOM 是否包含 CPE 的原因 —— 缺少 CPE 的 SBOM,后续漏洞扫描、合规检查都会受影响。

安装syft

在Windows系统上将Syft下载安装到D盘很简单,主要通过直接下载预编译的二进制文件来完成。下面是为您整理的详细步骤和后续使用方法。

📥 下载Syft二进制文件

这是最推荐的方法,直接获取可执行文件。

  1. 访问发布页面:打开 Syft 的官方 GitHub 发布页面:https://github.com/anchore/syft/releases

  2. 选择Windows版本:在最新的发布版本(例如 vx.x.x)下,找到适用于 Windows 的压缩包。它的名称通常类似于 syft_x.x.x_windows_amd64.zip

  3. 下载到D盘:点击这个链接开始下载。在保存时,请直接选择保存到D盘的目标文件夹,比如 `D:\Tools`。浏览器会自动将文件下载到你指定的位置。

📁 安装与配置

下载完成后,进行解压和配置。

  1. 解压文件:找到刚刚下载的 .zip文件,右键点击它,选择“全部提取”或使用解压软件(如WinRAR、7-Zip)解压。将解压目标也设置为D盘的同一个文件夹,例如 D:\Tools\`。解压后,你会得到一个名为syft.exe` 的可执行文件。

  2. 配置环境变量(关键步骤):为了能在任何路径下直接运行 syft命令,需要将它的所在目录添加到系统的 PATH 环境变量中。

    • 在任务栏的搜索框里输入“编辑系统环境变量”并打开它。

    • 在弹出的窗口中,点击下方的“环境变量”按钮。

    • 在“系统变量”区域,找到并选中名为 Path的变量,然后点击“编辑”。

    • 在新弹出的窗口中,点击“新建”,然后将Syft.exe所在的完整路径(例如 D:\Tools)添加进去。

    • 依次点击“确定”保存所有打开的窗口。

✅ 验证安装

配置完成后,需要检查一下是否成功。

  1. 按 Win + R键,输入 cmd然后按回车,打开命令提示符。

  2. 在新打开的窗口中,直接输入以下命令并按回车:

🔧 基本使用命令

安装成功后,你就可以使用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

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

主要用途

命令格式

关键参数/选项说明

使用示例

扫描容器镜像

grype <image-reference>

支持 Docker Hub、私有仓库、本地镜像存档 (docker-archive:)。--scope可指定扫描范围(如所有层)。

grype nginx:latest

扫描文件系统/目录

grype dir:<path>

扫描项目目录或解压后的文件系统,如检查 node_modules或 Python 虚拟环境。

grype dir:/path/to/your/project

扫描 SBOM 文件

grype sbom:<file-path>

直接分析由 Syft 等工具生成的软件物料清单文件,效率很高。

grype sbom:./sbom.json

控制输出与行为

grype [target] [options]

-o <format>: 指定输出格式(如 jsontable)。--fail-on <level>: 发现指定级别及以上漏洞时退出并报错。--only-fixed: 仅显示已有修复版本的漏洞。

grype my-app:latest -o json --fail-on high

扫描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:前端提交之后,就在“项目管理”部分新建一个项目,状态显示轮询的状态,我觉得后端不需要传给前端生成结果,前端在“项目管理”部分点击这个项目的“查看详情”,前端向后端发送查询请求,从后端数据库读取再返回给前端

调整逻辑

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐