Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d1d74acb9 |
@@ -1,3 +1,4 @@
|
|||||||
|
gradle.properties
|
||||||
db/
|
db/
|
||||||
.gradle
|
.gradle
|
||||||
build/
|
build/
|
||||||
|
|||||||
+7
-6
@@ -6,7 +6,6 @@ plugins {
|
|||||||
id 'eclipse'
|
id 'eclipse'
|
||||||
id 'idea'
|
id 'idea'
|
||||||
id 'war'
|
id 'war'
|
||||||
id 'org.cyclonedx.bom' version '3.2.4'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bootJar.enabled = true
|
bootJar.enabled = true
|
||||||
@@ -18,13 +17,17 @@ sourceCompatibility = "1.8"
|
|||||||
targetCompatibility = "1.8"
|
targetCompatibility = "1.8"
|
||||||
|
|
||||||
|
|
||||||
def nexusUrl = "https://nexus.eactive.synology.me:8090"
|
def reposiliteUrl = "http://localhost:8080"
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
url "${nexusUrl}/repository/maven-public/"
|
url "${reposiliteUrl}/private"
|
||||||
allowInsecureProtocol = true
|
allowInsecureProtocol = true
|
||||||
|
credentials {
|
||||||
|
username = reposiliteUser
|
||||||
|
password = reposilitePassword
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,6 +129,4 @@ war {
|
|||||||
bootWar {
|
bootWar {
|
||||||
archiveFileName = "elink-test-master.war"
|
archiveFileName = "elink-test-master.war"
|
||||||
from('src/main/resources/weblogic.xml') { into 'WEB-INF' }
|
from('src/main/resources/weblogic.xml') { into 'WEB-INF' }
|
||||||
}
|
}
|
||||||
// CycloneDX SBOM -> xlsx 변환 (gradle sbomXlsx)
|
|
||||||
apply from: "$projectDir/gradle/sbom-xlsx.gradle"
|
|
||||||
@@ -1 +1,4 @@
|
|||||||
org.gradle.java.home=C:\\Program Files\\Java\\jdk-17
|
org.gradle.java.home=C:\\Program Files\\Java\\jdk-17
|
||||||
|
|
||||||
|
reposiliteUser=admin
|
||||||
|
reposilitePassword=BFoZsHrQU4lmJf9rruDdcsPgE0gltKfRdTjAx04IgvFhrd5q07QwMFZqeLHFv+5n
|
||||||
|
|||||||
@@ -1,326 +0,0 @@
|
|||||||
/*
|
|
||||||
* CycloneDX SBOM(bom.json) -> Excel(xlsx) 변환 태스크.
|
|
||||||
*
|
|
||||||
* gradle sbomXlsx # cyclonedxBom 실행 후 변환
|
|
||||||
* gradle sbomXlsx -PsbomJson=path.json # 기존 bom.json 사용(cyclonedxBom 생략)
|
|
||||||
* gradle sbomXlsx -PsbomOut=out.xlsx # 출력 경로 지정
|
|
||||||
*
|
|
||||||
* buildscript 블록이 이 스크립트에만 적용되므로 POI 의존성이 메인 빌드
|
|
||||||
* classpath 나 WAR 산출물에는 포함되지 않는다.
|
|
||||||
*
|
|
||||||
* 시트: 요약 / WAR 기준
|
|
||||||
* 산출 기준은 war 태스크의 classpath(= runtimeClasspath) 이므로
|
|
||||||
* test·annotationProcessor·developmentOnly·compileOnly 의존은 모두 제외된다.
|
|
||||||
* bom.json 은 라이선스/해시/설명/직접-전이 판별을 위한 메타 소스로만 쓴다.
|
|
||||||
*/
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
url "https://nexus.eactive.synology.me:8090/repository/maven-public/"
|
|
||||||
allowInsecureProtocol = true
|
|
||||||
}
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath 'org.apache.poi:poi-ooxml:3.17'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import groovy.json.JsonSlurper
|
|
||||||
import org.apache.poi.ss.usermodel.BorderStyle
|
|
||||||
import org.apache.poi.ss.usermodel.FillPatternType
|
|
||||||
import org.apache.poi.ss.usermodel.HorizontalAlignment
|
|
||||||
import org.apache.poi.ss.usermodel.IndexedColors
|
|
||||||
import org.apache.poi.ss.usermodel.VerticalAlignment
|
|
||||||
import org.apache.poi.ss.util.CellRangeAddress
|
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
|
||||||
|
|
||||||
// 엑셀 셀 문자열 상한(32767)보다 여유를 둔 절단 길이
|
|
||||||
ext.SBOM_CELL_LIMIT = 32000
|
|
||||||
|
|
||||||
task sbomXlsx {
|
|
||||||
group = 'sbom'
|
|
||||||
description = 'CycloneDX bom.json 을 WAR 수록 기준 xlsx 로 변환한다'
|
|
||||||
|
|
||||||
// -PsbomJson 으로 기존 산출물을 지정하면 재생성하지 않는다
|
|
||||||
if (!project.hasProperty('sbomJson')) {
|
|
||||||
dependsOn 'cyclonedxBom'
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
File src = resolveBomJson(project)
|
|
||||||
File out = project.hasProperty('sbomOut')
|
|
||||||
? project.file(project.property('sbomOut'))
|
|
||||||
: new File(project.buildDir, "reports/sbom/${sbomFileName(project)}")
|
|
||||||
out.parentFile.mkdirs()
|
|
||||||
|
|
||||||
def bom = new JsonSlurper().parse(src, 'UTF-8')
|
|
||||||
def deploy = collectDeployJars(project)
|
|
||||||
def warRows = joinWarRows(deploy.jars, indexComponents(bom))
|
|
||||||
|
|
||||||
def wb = new XSSFWorkbook()
|
|
||||||
def st = createStyles(wb)
|
|
||||||
writeSummarySheet(wb, st, bom, warRows, src, deploy.label)
|
|
||||||
writeWarSheet(wb, st, warRows)
|
|
||||||
|
|
||||||
out.withOutputStream { os -> wb.write(os) }
|
|
||||||
wb.close()
|
|
||||||
|
|
||||||
int unmatched = warRows.count { it.matched == 'N' }
|
|
||||||
logger.lifecycle("SBOM xlsx 생성: ${out.absolutePath} " +
|
|
||||||
"(배포 수록 ${warRows.size()}개, SBOM 미매칭 ${unmatched}개, 원본 ${src.name})")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 산출 파일명: 배포 패키지명 기준 (war 있으면 war 파일명, 없으면 project 이름[-버전]) */
|
|
||||||
String sbomFileName(Project p) {
|
|
||||||
def warTask = p.tasks.findByName('war')
|
|
||||||
if (warTask != null) {
|
|
||||||
String archive = warTask.archiveFileName.get()
|
|
||||||
return archive.replaceAll(/\.(war|jar|ear)$/, '') + '-sbom.xlsx'
|
|
||||||
}
|
|
||||||
String ver = (p.version == null || p.version.toString() in ['', 'unspecified']) ? '' : "-${p.version}"
|
|
||||||
return "${p.name}${ver}-sbom.xlsx"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** bom.json 위치 결정: -PsbomJson > cyclonedxBom 산출 경로 후보 */
|
|
||||||
File resolveBomJson(Project p) {
|
|
||||||
if (p.hasProperty('sbomJson')) {
|
|
||||||
File f = p.file(p.property('sbomJson'))
|
|
||||||
if (!f.exists()) {
|
|
||||||
throw new GradleException("bom.json 없음: ${f.absolutePath}")
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
def candidates = [
|
|
||||||
new File(p.buildDir, 'reports/cyclonedx/bom.json'),
|
|
||||||
new File(p.buildDir, 'reports/bom.json'),
|
|
||||||
]
|
|
||||||
File found = candidates.find { it.exists() }
|
|
||||||
if (found == null) {
|
|
||||||
throw new GradleException(
|
|
||||||
"bom.json 을 찾지 못했다. 확인한 경로: " + candidates*.absolutePath.join(', ') +
|
|
||||||
"\n'gradle cyclonedxBom' 실행 후 재시도하거나 -PsbomJson=<경로> 로 지정한다.")
|
|
||||||
}
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 실제 배포물에 packaging 되는 jar 목록.
|
|
||||||
* war 프로젝트는 war 태스크 classpath(= runtimeClasspath), 그 외는 runtimeClasspath 를
|
|
||||||
* 기준으로 하므로 test/annotationProcessor/developmentOnly/compileOnly 는 자동으로 빠진다.
|
|
||||||
*
|
|
||||||
* @return [label: 기준 설명, jars: 행 목록]
|
|
||||||
*/
|
|
||||||
Map collectDeployJars(Project p) {
|
|
||||||
def cfg = p.configurations.findByName('runtimeClasspath')
|
|
||||||
if (cfg == null) {
|
|
||||||
p.logger.warn("[${p.name}] runtimeClasspath 가 없어 배포 기준 시트를 비운다")
|
|
||||||
return [label: '(없음)', jars: []]
|
|
||||||
}
|
|
||||||
|
|
||||||
def warTask = p.tasks.findByName('war')
|
|
||||||
def files
|
|
||||||
String label
|
|
||||||
if (warTask != null) {
|
|
||||||
files = warTask.classpath.files
|
|
||||||
label = 'WAR WEB-INF/lib (war 태스크 classpath)'
|
|
||||||
} else {
|
|
||||||
files = cfg.files
|
|
||||||
label = 'runtimeClasspath (war 태스크 없음)'
|
|
||||||
}
|
|
||||||
|
|
||||||
def coordByFile = [:]
|
|
||||||
cfg.resolvedConfiguration.resolvedArtifacts.each { a ->
|
|
||||||
def id = a.moduleVersion.id
|
|
||||||
coordByFile[a.file] = [group: id.group, name: id.name, version: id.version]
|
|
||||||
}
|
|
||||||
def jars = files.findAll { it.name.endsWith('.jar') }.collect { f ->
|
|
||||||
def c = coordByFile[f]
|
|
||||||
[
|
|
||||||
file : f.name,
|
|
||||||
group : c?.group ?: '',
|
|
||||||
name : c?.name ?: f.name.replaceAll(/\.jar$/, ''),
|
|
||||||
version: c?.version ?: '',
|
|
||||||
coord : c ? "${c.group}:${c.name}:${c.version}".toString() : '',
|
|
||||||
]
|
|
||||||
}.sort { it.file }
|
|
||||||
|
|
||||||
return [label: label, jars: jars]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** bom.json 컴포넌트를 'group:name:version' 키로 색인 (라이선스/해시/설명/직접-전이) */
|
|
||||||
Map indexComponents(bom) {
|
|
||||||
String rootRef = bom.metadata?.component?.'bom-ref'
|
|
||||||
Set directRefs = (bom.dependencies?.find { it.ref == rootRef }?.dependsOn ?: []) as Set
|
|
||||||
|
|
||||||
def index = [:]
|
|
||||||
bom.components?.each { c ->
|
|
||||||
def hashes = [:]
|
|
||||||
c.hashes?.each { h -> hashes[h.alg] = h.content }
|
|
||||||
|
|
||||||
def licenses = (c.licenses ?: []).collect { l ->
|
|
||||||
l.license?.id ?: l.license?.name ?: l.expression ?: ''
|
|
||||||
}.findAll { it }
|
|
||||||
|
|
||||||
index["${c.group ?: ''}:${c.name ?: ''}:${c.version ?: ''}".toString()] = [
|
|
||||||
direct : directRefs.contains(c.'bom-ref') ? '직접' : '전이',
|
|
||||||
licenses : licenses.join('; '),
|
|
||||||
licenseList: licenses.isEmpty() ? ['(미상)'] : licenses,
|
|
||||||
purl : c.purl ?: '',
|
|
||||||
sha256 : hashes['SHA-256'] ?: '',
|
|
||||||
sha1 : hashes['SHA-1'] ?: '',
|
|
||||||
description: c.description ?: '',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
|
|
||||||
/** WAR jar 목록에 SBOM 메타를 좌표로 결합 */
|
|
||||||
List joinWarRows(List warJars, Map index) {
|
|
||||||
def result = []
|
|
||||||
warJars.eachWithIndex { j, i ->
|
|
||||||
def m = j.coord ? index[j.coord] : null
|
|
||||||
result << [
|
|
||||||
no : i + 1,
|
|
||||||
file : j.file,
|
|
||||||
group : j.group,
|
|
||||||
name : j.name,
|
|
||||||
version : j.version,
|
|
||||||
direct : m?.direct ?: '',
|
|
||||||
licenses : m?.licenses ?: '',
|
|
||||||
licenseList: m?.licenseList ?: ['(미상)'],
|
|
||||||
purl : m?.purl ?: '',
|
|
||||||
sha256 : m?.sha256 ?: '',
|
|
||||||
sha1 : m?.sha1 ?: '',
|
|
||||||
matched : (m != null) ? 'Y' : 'N',
|
|
||||||
description: m?.description ?: '',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
Map createStyles(wb) {
|
|
||||||
def headFont = wb.createFont()
|
|
||||||
headFont.setBold(true)
|
|
||||||
headFont.setColor(IndexedColors.WHITE.getIndex())
|
|
||||||
|
|
||||||
def head = wb.createCellStyle()
|
|
||||||
head.setFont(headFont)
|
|
||||||
head.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex())
|
|
||||||
head.setFillPattern(FillPatternType.SOLID_FOREGROUND)
|
|
||||||
head.setAlignment(HorizontalAlignment.CENTER)
|
|
||||||
head.setVerticalAlignment(VerticalAlignment.CENTER)
|
|
||||||
head.setBorderBottom(BorderStyle.THIN)
|
|
||||||
|
|
||||||
def body = wb.createCellStyle()
|
|
||||||
body.setVerticalAlignment(VerticalAlignment.TOP)
|
|
||||||
|
|
||||||
def wrap = wb.createCellStyle()
|
|
||||||
wrap.setVerticalAlignment(VerticalAlignment.TOP)
|
|
||||||
wrap.setWrapText(true)
|
|
||||||
|
|
||||||
def labelFont = wb.createFont()
|
|
||||||
labelFont.setBold(true)
|
|
||||||
def label = wb.createCellStyle()
|
|
||||||
label.setFont(labelFont)
|
|
||||||
|
|
||||||
return [head: head, body: body, wrap: wrap, label: label]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 헤더 행 생성 + 폭 지정 + 틀고정 */
|
|
||||||
def writeHeader(sheet, style, List<String> headers, List<Integer> widths) {
|
|
||||||
def row = sheet.createRow(0)
|
|
||||||
row.setHeightInPoints(20f)
|
|
||||||
headers.eachWithIndex { h, i ->
|
|
||||||
def cell = row.createCell(i)
|
|
||||||
cell.setCellValue(h)
|
|
||||||
cell.setCellStyle(style)
|
|
||||||
sheet.setColumnWidth(i, widths[i] * 256)
|
|
||||||
}
|
|
||||||
sheet.createFreezePane(0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
def cellOf(row, int idx, value, style) {
|
|
||||||
def cell = row.createCell(idx)
|
|
||||||
String s = (value == null) ? '' : value.toString()
|
|
||||||
if (s.length() > SBOM_CELL_LIMIT) {
|
|
||||||
s = s.substring(0, SBOM_CELL_LIMIT) + '…(생략)'
|
|
||||||
}
|
|
||||||
cell.setCellValue(s)
|
|
||||||
cell.setCellStyle(style)
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
def writeSummarySheet(wb, st, bom, List warRows, File src, String basisLabel) {
|
|
||||||
def sheet = wb.createSheet('요약')
|
|
||||||
def comp = bom.metadata?.component ?: [:]
|
|
||||||
def tool = bom.metadata?.tools?.components?.getAt(0)
|
|
||||||
Set licenseKinds = warRows.collectMany { it.licenseList } as Set
|
|
||||||
|
|
||||||
def items = [
|
|
||||||
['대상 프로젝트', "${comp.group ?: ''}:${comp.name ?: ''}:${comp.version ?: ''}"],
|
|
||||||
['산출 기준', "${basisLabel} — test/annotationProcessor/compileOnly 제외"],
|
|
||||||
['BOM 포맷', "${bom.bomFormat ?: ''} ${bom.specVersion ?: ''}"],
|
|
||||||
['serialNumber', bom.serialNumber ?: ''],
|
|
||||||
['생성 시각', bom.metadata?.timestamp ?: ''],
|
|
||||||
['생성 도구', tool ? "${tool.name} ${tool.version}" : ''],
|
|
||||||
['원본 파일', src.absolutePath],
|
|
||||||
['배포 수록 jar', warRows.size()],
|
|
||||||
[' └ 직접 의존', warRows.count { it.direct == '직접' }],
|
|
||||||
[' └ 전이 의존', warRows.count { it.direct == '전이' }],
|
|
||||||
[' └ SBOM 미매칭', warRows.count { it.matched == 'N' }],
|
|
||||||
['라이선스 종류', licenseKinds.size()],
|
|
||||||
['라이선스 미상', warRows.count { it.licenses.isEmpty() }],
|
|
||||||
]
|
|
||||||
|
|
||||||
writeHeader(sheet, st.head, ['항목', '값'], [30, 90])
|
|
||||||
items.eachWithIndex { item, i ->
|
|
||||||
def row = sheet.createRow(i + 1)
|
|
||||||
cellOf(row, 0, item[0], st.label)
|
|
||||||
cellOf(row, 1, item[1], st.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 라이선스 분포 (요약 하단)
|
|
||||||
def byLicense = [:].withDefault { 0 }
|
|
||||||
warRows.each { r -> r.licenseList.each { lic -> byLicense[lic] = byLicense[lic] + 1 } }
|
|
||||||
def sorted = byLicense.entrySet().sort { a, b -> (b.value <=> a.value) ?: (a.key <=> b.key) }
|
|
||||||
|
|
||||||
int base = items.size() + 2
|
|
||||||
def hdr = sheet.createRow(base)
|
|
||||||
cellOf(hdr, 0, '라이선스', st.head)
|
|
||||||
cellOf(hdr, 1, 'jar 수', st.head)
|
|
||||||
sorted.eachWithIndex { e, i ->
|
|
||||||
def row = sheet.createRow(base + 1 + i)
|
|
||||||
cellOf(row, 0, e.key, st.body)
|
|
||||||
cellOf(row, 1, e.value, st.body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 실제 배포물(WAR WEB-INF/lib) 기준 시트 */
|
|
||||||
def writeWarSheet(wb, st, List warRows) {
|
|
||||||
def sheet = wb.createSheet('WAR 기준')
|
|
||||||
def headers = ['No', 'jar 파일명', 'Group', 'Name', 'Version', '구분',
|
|
||||||
'License', 'purl', 'SHA-256', 'SHA-1', 'SBOM매칭', 'Description']
|
|
||||||
def widths = [6, 46, 32, 34, 16, 7, 30, 60, 40, 30, 10, 60]
|
|
||||||
writeHeader(sheet, st.head, headers, widths)
|
|
||||||
|
|
||||||
warRows.eachWithIndex { r, i ->
|
|
||||||
def row = sheet.createRow(i + 1)
|
|
||||||
cellOf(row, 0, r.no, st.body)
|
|
||||||
cellOf(row, 1, r.file, st.body)
|
|
||||||
cellOf(row, 2, r.group, st.body)
|
|
||||||
cellOf(row, 3, r.name, st.body)
|
|
||||||
cellOf(row, 4, r.version, st.body)
|
|
||||||
cellOf(row, 5, r.direct, st.body)
|
|
||||||
cellOf(row, 6, r.licenses, st.body)
|
|
||||||
cellOf(row, 7, r.purl, st.body)
|
|
||||||
cellOf(row, 8, r.sha256, st.body)
|
|
||||||
cellOf(row, 9, r.sha1, st.body)
|
|
||||||
cellOf(row, 10, r.matched, st.body)
|
|
||||||
cellOf(row, 11, r.description, st.wrap)
|
|
||||||
}
|
|
||||||
if (!warRows.isEmpty()) {
|
|
||||||
sheet.setAutoFilter(new CellRangeAddress(0, warRows.size(), 0, headers.size() - 1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"D:\installs\jdk-21\bin\java" -jar ./build/libs/elink-test-master.jar
|
||||||
Reference in New Issue
Block a user