为什么不能用字符串比较版本号
SELECT * FROM assets WHERE version < '2.15.0'
字符串比较结果:
'2.10.0' < '2.9.0' -- 错误!字典序 '1' < '9'
SemVer(语义化版本)中 2.10.0 > 2.9.0,必须按数字分段比较。
方案一:程序端比较(推荐)
数据库只存原始版本字符串,程序端做 SemVer 比较:
Java(semver4j):
<dependency>
<groupId>org.semver4j</groupId>
<artifactId>semver4j</artifactId>
<version>5.3.0</version>
</dependency>
import org.semver4j.Semver;
public class VersionMatcher {
public static boolean isVulnerable(String version, String range) {
try {
Semver v = Semver.parse(version);
return v.satisfies(range);
} catch (Exception e) {
return false;
}
}
}
// 使用
isVulnerable("2.14.1", ">=2.0.0 <2.15.0") // true
isVulnerable("2.15.0", ">=2.0.0 <2.15.0") // false
Python(packaging 库):
from packaging.version import Version
from packaging.specifiers import SpecifierSet
def is_vulnerable(version: str, spec: str) -> bool:
try:
return Version(version) in SpecifierSet(spec)
except Exception:
return False
is_vulnerable("2.14.1", ">=2.0.0,<2.15.0") # True
方案二:拆成上下界存数据库
将范围条件拆为四个字段,程序比较时不需要解析字符串:
CREATE TABLE vuln_rule (
id BIGINT PRIMARY KEY,
component VARCHAR(100),
min_version VARCHAR(50),
max_version VARCHAR(50),
min_include TINYINT DEFAULT 1, -- 1=包含 >=,0=不包含 >
max_include TINYINT DEFAULT 0 -- 1=包含 <=,0=不包含 <
);
表示 >= 2.0.0 < 2.15.0:
| min_version | max_version | min_include | max_include |
|---|---|---|---|
| 2.0.0 | 2.15.0 | 1 | 0 |
程序判断:
boolean matches(String version, VulnRule rule) {
Semver v = Semver.parse(version);
Semver min = Semver.parse(rule.getMinVersion());
Semver max = Semver.parse(rule.getMaxVersion());
boolean lowerOk = rule.isMinInclude() ? v.isGreaterThanOrEqualTo(min) : v.isGreaterThan(min);
boolean upperOk = rule.isMaxInclude() ? v.isLowerThanOrEqualTo(max) : v.isLowerThan(max);
return lowerOk && upperOk;
}
方案三:版本号数字化后数据库直接查
将 2.15.0 转换为定宽整数 002015000,再存数据库:
public static long versionToLong(String version) {
String[] parts = version.split("\\.");
long major = parts.length > 0 ? Long.parseLong(parts[0]) : 0;
long minor = parts.length > 1 ? Long.parseLong(parts[1]) : 0;
long patch = parts.length > 2 ? Long.parseLong(parts[2]) : 0;
return major * 1_000_000L + minor * 1_000L + patch;
}
// "2.15.0" -> 2015000
// "2.9.0" -> 2009000
// "2.10.0" -> 2010000 (正确:2010000 > 2009000)
数据库存 version_num BIGINT,直接用 SQL 范围查询:
SELECT * FROM vuln_rule
WHERE version_num >= 2000000 AND version_num < 2015000;
适合版本号分段均不超过 999 的情况。
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 程序端比较 | 支持复杂范围(pre-release) | 无法在 SQL 直接过滤 |
| 上下界字段 | 数据结构清晰,易扩展 | 多字段 JOIN 稍复杂 |
| 数字化版本 | SQL 直接查询,性能最好 | 分段超过 999 会溢出 |
推荐:中小规模用方案一(程序端比较),大规模漏洞库(百万级 asset)用方案三配合索引加速。
漏洞管理平台实践
批量扫描 asset 时,避免 N+1 查询:
// 一次查出所有漏洞规则
List<VulnRule> rules = vulnRuleMapper.selectAll();
// 按 component 分组
Map<String, List<VulnRule>> ruleMap = rules.stream()
.collect(Collectors.groupingBy(VulnRule::getComponent));
// 批量匹配
for (Asset asset : assets) {
List<VulnRule> candidates = ruleMap.getOrDefault(asset.getComponent(), List.of());
for (VulnRule rule : candidates) {
if (VersionMatcher.isVulnerable(asset.getVersion(), rule.getRange())) {
record(asset, rule);
}
}
}