clair扫描的相关基础请先移步我的另外一篇文章镜像安全扫描工具clair与clairctl
这次我们采用clair api方式的扫描,基本思路是
我们需要三个容器,分别是
docker run -d --name tomcat8 -p 6062:8080 -v /opt/deploy/clair/scanImage:/usr/local/tomcat/webapps/ROOT tomcat:jdk8-corretto
docker run -d -e POSTGRES_PASSWORD="password" -p 5432:5432 -v /opt/deploy/clair/postgres/data:/var/lib/postgresql/data --name postgres vmware/postgresql-photon:v1.5.0
新建目录/opt/deploy/clair/config,在该目录下新建文件config.yml
database:
type: pgsql
options:
source: postgresql://postgres:password@127.0.0.1:5432/postgres?sslmode=disable
cachesize: 16384
api:
port: 6060
healthport: 6061
timeout: 900s
updater:
interval: 2h
notifier:
attempts: 3
renotifyinterval: 2h
docker run --net=host -d -p 6060-6061:6060-6061 -v /opt/deploy/clair/config:/config quay.io/coreos/clair:v2.0.1 -config=/config/config.yml
其中6060为扫描端口,6061为健康检查端口
注意,Postgres必须在clair之前启动,否则clair会连不上数据库,直接停止容器。
clair与postgres正常启动后,clair会自动去拉取漏洞数据插入数据库中,这一过程往往持续几个小时,且由于网络原因,大量漏洞数据无法拉取,此时需要另辟蹊径。
(1)检查工作
(2)调用脚本
private String executeRemoteShell() {
String result = null;
SSH ssh = new SSH(主机ip, 用户名, 密码);
try {
boolean isConnect = ssh.connect();
if (isConnect) {
result = ssh.executeWithResult(脚本全路径,传入参数);
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
#!/bin/bash
baseDir=/opt/deploy/clair
if [ $? -eq 0 ];then
docker pull ${1} > /dev/null 2>&1
imageName=`echo ${1} | sed 's!\/!_!g'` > /dev/null 2>&1
imageDir="${imageName}" > /dev/null 2>&1
mkdir ${baseDir}/scanImage/${imageDir} > /dev/null 2>&1
docker save -o ${baseDir}/scanImage/${imageDir}/${imageDir}.tar ${1} > /dev/null 2>&1
tar -xvf ${baseDir}/scanImage/${imageDir}/${imageDir}.tar -C ${baseDir}/scanImage/${imageDir}/ > /dev/null 2>&1
cat ${baseDir}/scanImage/${imageDir}/manifest.json
exit 0
else
echo 'scan image failed!'
exit 6
fi
(3)使用clair api提交镜像分层数据,postLayers为false时,此时已经提交过分层数据,可以利用最后一层的id直接查询漏洞。(postgres会将已经查询过的镜像漏洞存储,提交完分层数据后,可以使用最后一层的id获取漏洞数据)
//将镜像的分层文件提交至clair,进行分析并返回结果
private ImageScanResult getScanResultFromClair(ImageScan imageScan, boolean postLayers) throws Exception {
String imageName = imageScan.getImageName();
String imageTag = imageScan.getImageTag();
//1.获取manifest层序描述文件内容
String result = RestTemplateUtils.get(getManifestPath(imageName, imageTag));
JSONArray jsonArray = JSONArray.parseArray(result);
if (jsonArray == null || jsonArray.size() == 0) {
throw new Exception("无法获取或解析镜像层序的描述文件");
}
JSONObject jsonObject = (JSONObject) jsonArray.get(0);
@SuppressWarnings("unchecked")
List<String> layers = (List<String>) jsonObject.get("Layers");
//提交的api
String clairApi = "http://" + systemConfig.getRelayServerIp() + ":" + systemConfig.getClairPort() + "/v1/layers";
if (postLayers) {
//上一层id
String beforePath = "";
//2.向clair逐层提交
for (String layer : layers) {
String name = layer.substring(0, layer.indexOf("/"));
String path = getAbsoluteLayerPath(layer, imageScan.getImageName(), imageScan.getImageTag());
String parentName = beforePath;
String format = "Docker";
beforePath = name;
Map<String, Map<String, String>> postParameter = new HashMap<>();
Map<String, String> layerParameter = new HashMap<>();
layerParameter.put("Name", name);
layerParameter.put("Path", path);
layerParameter.put("ParentName", parentName);
layerParameter.put("Format", format);
postParameter.put("Layer", layerParameter);
JSONObject layerJSONObject = JSONObject.parseObject(JSONObject.toJSONString(postParameter));
RestTemplateUtils.post(clairApi, layerJSONObject, null);
}
}
//3.所有层提交完毕后,使用最后一层进行查询漏洞
int last = layers.size() - 1;
String lastName = layers.get(last).substring(0, layers.get(last).indexOf("/"));
String leak = RestTemplateUtils.get(clairApi + "/" + lastName + "?features&vulnerabilities");
JSONObject leakJSONObject = JSONObject.parseObject(leak);
JSONObject leakLayerJSONObject = leakJSONObject.getJSONObject("Layer");
JSONArray featuresJSONArray = leakLayerJSONObject.getJSONArray("Features");
List<Feature> featureList = new ArrayList<>();
Feature feature;
for (Object f : featuresJSONArray) {
JSONObject featureJSONObject = (JSONObject) f;
feature = new Feature();
String name = featureJSONObject.getString("Name");
String namespaceName = featureJSONObject.getString("NamespaceName");
String versionFormat = featureJSONObject.getString("VersionFormat");
String version = featureJSONObject.getString("Version");
String addedBy = featureJSONObject.getString("AddedBy");
List<Vulnerability> vulnerabilities = new ArrayList<>();
JSONArray vulnerabilitiesJSONArray = featureJSONObject.getJSONArray("Vulnerabilities");
if (vulnerabilitiesJSONArray == null || vulnerabilitiesJSONArray.size() == 0) {
vulnerabilitiesJSONArray = new JSONArray();
}
Vulnerability vulnerability;
for (Object v : vulnerabilitiesJSONArray) {
JSONObject vulnerabilitiesJSONObject = (JSONObject) v;
vulnerability = new Vulnerability();
String vulnerabilityName = vulnerabilitiesJSONObject.getString("Name");
String vulnerabilityNamespaceName = vulnerabilitiesJSONObject.getString("NamespaceName");
String vulnerabilityDescription = vulnerabilitiesJSONObject.getString("Description");
String vulnerabilityLink = vulnerabilitiesJSONObject.getString("Link");
String vulnerabilitySeverity = vulnerabilitiesJSONObject.getString("Severity");
String vulnerabilityFixedBy = vulnerabilitiesJSONObject.getString("FixedBy");
vulnerability.setName(vulnerabilityName);
vulnerability.setNamespaceName(vulnerabilityNamespaceName);
vulnerability.setDescription(vulnerabilityDescription);
vulnerability.setLink(vulnerabilityLink);
vulnerability.setSeverity(vulnerabilitySeverity);
vulnerability.setFixedBy(vulnerabilityFixedBy);
vulnerabilities.add(vulnerability);
}
feature.setName(name);
feature.setNamespaceName(namespaceName);
feature.setVersionFormat(versionFormat);
feature.setVersion(version);
feature.setAddedBy(addedBy);
feature.setVulnerabilities(vulnerabilities);
featureList.add(feature);
}
//4.封装镜像扫描结果
ImageScanResult imageScanResult = new ImageScanResult();
int featureNum = featureList.size();
long vulnerabilityNum = 0;
long high = 0;
long medium = 0;
long low = 0;
for (Feature temp : featureList) {
List<Vulnerability> vulnerabilityList = temp.getVulnerabilities();
vulnerabilityNum += vulnerabilityList.size();
high += vulnerabilityList.stream().filter(v -> v.getSeverity().equals("High")).count();
medium += vulnerabilityList.stream().filter(v -> v.getSeverity().equals("Medium")).count();
low += vulnerabilityList.stream().filter(v -> v.getSeverity().equals("Low")).count();
}
imageScanResult.setImageName(imageScan.getImageName());
imageScanResult.setImageTag(imageScan.getImageTag());
imageScanResult.setFeatureNum(featureNum);
imageScanResult.setVulnerabilityNum(vulnerabilityNum);
imageScanResult.setHigh(high);
imageScanResult.setMedium(medium);
imageScanResult.setLow(low);
imageScanResult.setFeatureList(featureList);
return imageScanResult;
}
(4)善后工作
清理扫描成功后的文件,这时我们打算清理解压出来的所有的tar包文件,否则解压目录将会异常庞大。博主在清理完一个24G的目录后,仅剩下几M的额外文件。
为什么我们不直接删除所有解压目录?这是因为我们需要留下镜像的分层描述文件(manifest.json),这个文件存放着该镜像的分层信息,包括分层id,分层tar包的地址等。后期我们可以依据此文件拿到最后一层的id,并调用clair api直接获取到该镜像的扫描结果。如果我们将扫描结果单独入库的话,那可以在扫描成功后删除整个解压目录。
//扫描完成后删除镜像解压目录
private void delImageDirAfterScan(ImageScan imageScan) {
String imageIdentification = formatImageNameAndTag(imageScan.getImageName(), imageScan.getImageTag());
String imageDir = systemConfig.getImageScanShellPath() + "scanImage/" + imageIdentification + "/";
SSH ssh = new SSH(systemConfig.getRelayServerIp(), systemConfig.getRelayServerUser(), systemConfig.getRelayServerPwd());
try {
boolean isConnect = ssh.connect();
if (isConnect) {
ssh.execute("find " + imageDir + " -name '*.tar' -type f -print -exec rm -rf {} \\;");
}
} catch (IOException e) {
e.printStackTrace();
}
}