plugins { id 'java' id 'war' id 'eclipse' id 'idea' id 'org.cyclonedx.bom' version '3.2.4' id 'org.springframework.boot' version '2.7.18' id 'io.spring.dependency-management' version '1.1.3' } group = 'com.eactive' version = '1.0.0' sourceCompatibility = "1.8" targetCompatibility = "1.8" def nexusUrl = "https://nexus.eactive.synology.me:8090" allprojects { repositories { maven { url "${nexusUrl}/repository/maven-public/" allowInsecureProtocol = true } } } // 로컬 전용 라이브러리 — WebLogic 배포 산출물(war/bootWar)에서 제외한다. // 정보보호 점검에서 Actuator/모니터링 라이브러리가 배포본에 실려 있으면 불필요하게 탐지되므로 // 로컬(bootRun/IDE)에서만 classpath 에 오르게 한다. 제외 로직은 아래 war/bootWar 블록. // // ※ Spring Boot 의 developmentOnly 를 쓰지 않는 이유 // developmentOnly 는 bootJar/bootWar 에서만 제외된다. 표준 war task 는 runtimeClasspath 를 // 그대로 쓰므로 산출물에 실린다 (실측: devtools 가 eapim-portal.war 에 포함되어 있었음). // ※ 별도 configuration(localOnly)을 쓰지 않는 이유 // runtimeClasspath 에서 빠지면 IntelliJ 가 모듈 classpath 를 구성할 때도 빠져 // IDE 로 기동할 때 Actuator/SBA 가 동작하지 않는다. def localOnlyLibPrefixes = [ 'spring-boot-devtools', 'spring-boot-starter-actuator', 'spring-boot-actuator', // spring-boot-actuator, -autoconfigure 모두 매칭 'micrometer-', // actuator 전용(runtimeClasspath 상 다른 출처 없음 — 확인함) 'spring-boot-admin-', ] dependencies { annotationProcessor "org.projectlombok:lombok:1.18.28" annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0", "org.mapstruct:mapstruct-processor:1.5.5.Final" implementation 'com.eactive.elink.common:elink-common-data:4.5.5' // OpenAPI 3.0/3.1 spec 지원 (Nexus/Maven 해석). 기존 libs/swagger fileTree 대체. // swagger 최신 릴리스(2.2.52/2.1.45)도 SpecVersion 은 V30/V31 까지만 — OpenAPI 3.2 는 // 아직 swagger-core/parser 어느 버전도 미지원. 지원 추가 시 버전만 상향하면 됨. JDK8 호환. implementation 'io.swagger.core.v3:swagger-core:2.2.52' // io.swagger.v3.oas.models.*, io.swagger.v3.core.util.Json implementation 'io.swagger.parser.v3:swagger-parser:2.1.45' // io.swagger.parser.OpenAPIParser, io.swagger.v3.parser.* implementation 'io.swagger:swagger-annotations:1.6.14' // io.swagger.annotations.ApiModelProperty (CommissionSearch) implementation project(':elink-online-core-jpa') implementation project(':elink-portal-common') // implementation project(':kjb-safedb') implementation('org.springframework.boot:spring-boot-starter') implementation('org.springframework.boot:spring-boot-starter-web') implementation('org.springframework.boot:spring-boot-starter-validation') // ↓ 로컬 전용. war/bootWar 산출물에서는 localOnlyLibPrefixes 로 제외된다. // runtimeOnly 인 이유: compileClasspath 에서 빠지므로 자바 코드가 이 API 를 참조하면 // 컴파일 단계에서 막힌다. implementation 이면 참조가 컴파일에 통과해버리고, // 배포본(actuator/SBA 제외됨)에서 NoClassDefFoundError 로 터진다. // IntelliJ / bootRun 은 runtimeClasspath 기준이라 로컬 기동에는 정상 포함된다. // 설정(application-local*.yml)으로만 사용한다. runtimeOnly('org.springframework.boot:spring-boot-starter-actuator') // Spring Boot Admin client. 2.7.16 = Spring Boot 2.7.x 대응 마지막 계열(Java 8 호환). runtimeOnly('de.codecentric:spring-boot-admin-starter-client:2.7.16') // 위 runtimeOnly 원칙의 유일한 예외. InterceptorsEndpointConfig 가 actuator API // (@Endpoint / InfoContributor)를 참조해야 하므로 compileClasspath 에만 올린다. // compileOnly 는 runtimeClasspath 에 포함되지 않으므로 war/bootWar 산출물에는 영향이 없다. // 해당 클래스는 @ConditionalOnClass 로 actuator 부재 시 로드되지 않으니 // 배포본(actuator 제외)에서 NoClassDefFoundError 가 나지 않는다. compileOnly('org.springframework.boot:spring-boot-actuator') implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0' implementation group: 'com.fasterxml.woodstox', name: 'woodstox-core', version: '6.5.1' // Thymeleaf 3.1 코어가 #temporals 를 내장 제공 → java8time extras 를 함께 두면 표현식 객체가 중복 등록된다. implementation('org.springframework.boot:spring-boot-starter-thymeleaf') { exclude group: 'org.thymeleaf.extras', module: 'thymeleaf-extras-java8time' } implementation 'org.springframework.boot:spring-boot-starter-security' implementation('org.springframework.boot:spring-boot-starter-cache') implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.data:spring-data-envers' implementation 'org.springframework.boot:spring-boot-starter-jdbc' developmentOnly 'org.springframework.boot:spring-boot-devtools' // spring-expression 개별 pin 제거: 아래 ext 의 spring-framework.version 이 전 모듈을 일괄 관리한다. // 개별 pin 이 남아 있으면 다른 spring-* 모듈보다 낮은 버전으로 고정되어 버전이 어긋난다. implementation group: 'xalan', name: 'xalan', version: '2.7.3' implementation 'org.hibernate:hibernate-envers:5.6.15.Final' implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.4.0' // 3.0.0 은 thymeleaf 3.0 전용 implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' // uap-java 제거: 소스 전체에 ua_parser 참조 0건이고, 1.5.3 은 snakeyaml 2.x 에서 삭제된 // SafeConstructor() no-arg 를 호출해 NoSuchMethodError 를 낸다. 되살릴 경우 1.6.1 이상. implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.1' implementation 'javax.servlet:javax.servlet-api:4.0.0' implementation 'org.jasypt:jasypt:1.9.3' implementation 'xerces:xercesImpl:2.12.2' // weblogic.xml prefer-application-packages 가 com.fasterxml.jackson.* 를 앱 우선으로 강제한다 // (oracle_common jackson-databind 2.17.0 vs 앱 2.18.10 split-package 방지, WLog 실사용 확인). // Thymeleaf StandardJavaScriptSerializer 가 ObjectMapper.findAndRegisterModules() 로 // jackson-module-jaxb-annotations 를 옵션 모듈로 자동탐색하는데, 이 모듈은 WebLogic 번들에만 // 있고 앱엔 없어 ServiceConfigurationError(Provider not found) 발생 → 앱에도 명시 추가해 // jackson 전체를 자체 완결시킨다(jackson-bom 2.18.10 로 버전 통일). implementation 'com.fasterxml.jackson.module:jackson-module-jaxb-annotations' // Uncontrolled recursion in ClassUtils.getClass(...) on very long inputs. 3.18.0+ 에서 수정. implementation 'org.apache.commons:commons-lang3:3.20.0' implementation 'org.apache.commons:commons-collections4:4.4' implementation 'commons-net:commons-net:3.9.0' // CVE-2025-48734 (PropertyUtilsBean 이 enum 의 declaredClass 프로퍼티 노출 → ClassLoader 접근/RCE). // 1.11.0 부터 SuppressPropertiesBeanIntrospector 가 기본 활성이라 declaredClass 접근이 차단된다. // 이 앱의 호출부(PasswordMatchValidator / PasswordRuleValidator / AuthNumberValidator)는 // 어노테이션에 박힌 고정 프로퍼티명만 넘기므로 외부 입력 경로는 없지만 버전은 올려 둔다. // 1.11.0 = Java 8 바이트코드(major 52), PropertyUtils.getProperty/getNestedProperty API 동일. implementation('commons-beanutils:commons-beanutils:1.11.0') { // exclude group: 'commons-collections', module: 'commons-collections' } implementation 'org.mapstruct:mapstruct:1.5.5.Final' // jackson 개별 pin 제거: 아래 ext 의 jackson-bom.version 이 전 모듈을 일괄 관리한다. // 개별 pin 은 BOM 보다 우선하므로 남겨 두면 BOM 만 올렸을 때 core/annotations/databind 가 // 옛 버전에 고정돼 버전이 어긋난다(실제로 그런 상태였다). implementation group: 'org.apache.velocity', name: 'velocity-engine-core', version: '2.3' // runtimeOnly group: 'com.mysql', name: 'mysql-connector-j', version: '8.4.0' runtimeOnly 'com.oracle.database.jdbc:ojdbc8:19.20.0.0' implementation 'net.bytebuddy:byte-buddy:1.14.5' // Commons FileUpload (WAS 독립적인 multipart 처리) // CVE-2025-48976 (멀티파트 파트 헤더 크기 제한 부재 → DoS). 1.6.0 에서 partHeaderSizeMax 도입. // 주의: 1.6 부터 파트 헤더 총량 기본 상한이 10240 → 512 바이트로 줄었다(DEFAULT_PART_HEADER_SIZE_MAX). // 한글 파일명은 UTF-8 로 3바이트/자라 Content-Disposition 이 길어질 수 있어 실측으로 여유를 확인했다. implementation 'commons-fileupload:commons-fileupload:1.6.0' implementation 'commons-io:commons-io:2.15.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' testImplementation 'org.mockito:mockito-inline:3.11.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.seleniumhq.selenium:selenium-java:4.8.3' testImplementation group: 'io.github.bonigarcia', name: 'webdrivermanager', version: '5.9.1' testImplementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1' } ext { springMavenArtifactVersion = '5.3.30' encoding = 'UTF-8' profile = 'local' // 내장 Tomcat 버전 상향 (Spring Boot 2.7.18 기본값 9.0.83 → 9.0.120). // Boot BOM 의 tomcat.version 프로퍼티를 덮어써서 tomcat-embed-core/-el/-websocket 이 함께 올라간다. // 9.0.x 계열 유지 = Servlet 4.0 / javax.* 네임스페이스 그대로, JDK8 호환. set('tomcat.version', '9.0.120') // CVE-2022-1471 (snakeyaml Constructor 임의 타입 역직렬화 → RCE). Boot 2.7.18 BOM 기본값 1.30 → 2.6. // 앱/프레임워크 실경로는 이미 SafeConstructor 계열(OriginTrackedYamlLoader, spring-beans // FilteringConstructor, swagger-parser DeserializationUtils)이라 익스플로잇 경로는 없었으나 // SCA 는 버전으로 판정하므로 2.x 로 올린다. snakeyaml 2.6 = Java 8 바이트코드(major 52). // 2.6 인 이유: swagger-core 2.2.52 / swagger-parser-v3 2.1.45 가 요구하는 버전이라 강등이 없다. // 부수 효과: swagger-parser 가 호출하는 LoaderOptions.setCodePointLimit(1.32+ API) 도 해소. set('snakeyaml.version', '2.6') // jackson 전 모듈 버전 통일(Boot 2.7.18 BOM 기본 2.13.5). 2.18.x 는 JDK8 호환 라인이다. // 이유 3가지 // 1) snakeyaml 2.x 는 ParserImpl(StreamReader) 를 제거했고 jackson-dataformat-yaml 은 // 2.15+ 부터 ParserImpl(StreamReader, LoaderOptions) 를 쓴다 — 위 snakeyaml 상향의 전제. // 2) WS-2026-0003 (jackson-core async parser DoS, CVSS 7.5) — 2.18.6 에서 수정. // 3) jackson-databind PolymorphicTypeValidator 우회(제네릭 타입 인자 미검증) — 2.18.8 에서 수정. // 이 앱은 다형성 역직렬화(activateDefaultTyping/@JsonTypeInfo)를 쓰지 않아 노출 경로는 없다. // 2.18.x 마지막 패치를 쓴다. set('jackson-bom.version', '2.18.10') // Thymeleaf SSTI (≤3.1.3.RELEASE: 표현식 접근 객체 제한 우회 → 템플릿 인젝션). 3.0.x 는 EOL 이라 // 백포트가 없어 3.1.4 로 올린다. JDK8/Spring5 유지: thymeleaf 3.1.4 / thymeleaf-spring5 3.1.4 / // extras-springsecurity5 3.1.5 / layout-dialect 3.4.0 모두 Java8 바이트코드(major 52), 패키지도 // org.thymeleaf.spring5 + javax.servlet 그대로다. // Boot 2.7 ThymeleafAutoConfiguration 이 호출하는 setter 는 3.1.4 에 전부 존재함(확인함). // 주의: 3.1 은 #request/#session/#response/#servletContext 표현식 객체를 제거했다(IllegalArgumentException). set('thymeleaf.version', '3.1.4.RELEASE') set('thymeleaf-extras-springsecurity.version', '3.1.5.RELEASE') // Spring Framework 5.3.x OSS 마지막 릴리스로 통일(Boot 2.7.18 BOM 기본 5.3.31, 일부 5.3.30 혼재였음). // 5.3.32~5.3.39 구간 CVE 정리용. 5.3.x 는 OSS EOL 이라 이 위로는 상용(Enterprise) 빌드뿐이다. // 남는 5.3.x 지적(CVE-2026-41855 JMS Jackson 역직렬화, CVE-2016-1000027 HttpInvoker)은 // 5.3 계열에 수정본이 없고 앱이 JMS/HttpInvoker 를 쓰지 않으므로 억제 항목으로 따로 관리한다. set('spring-framework.version', '5.3.39') // Spring Security 5.7.x OSS 마지막 릴리스(Boot 2.7.18 BOM 기본 5.7.11). // CVE-2026-22732(응답 커밋 후 보안 헤더 미기록)는 수정본이 5.7.22/5.8.24 = Enterprise 전용이라 // OSS 로는 올릴 수 없다. 우회책(HeaderWriterFilter.shouldWriteHeadersEagerly=true)은 동작 변경이라 분리 검토. set('spring-security.version', '5.7.14') } task printProfile { doLast { if (project.hasProperty('profile')) { println "Current profile: ${project.property('profile')}" } else { println "No profile specified" } } } bootRun { // 디버깅 사용시에는 아래 내용 주석 해제 하세요 // jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005' sourceResources sourceSets.main // processResources 필터 우회 (dev yml 직접 참조) // 로컬 기동 기본 프로파일. // dev/stage/prod 는 리버스프록시(OHS) 뒤 WAR 배포용이라 server.forward-headers-strategy=framework 가 // 걸려 있다. framework 는 신뢰 프록시 목록 없이 X-Forwarded-* 를 그대로 신뢰하므로 앞단이 없는 // 로컬 기동에는 쓰지 않는다(로컬은 공통 기본값 native = Tomcat RemoteIpValve, 사설대역만 신뢰). // 다른 프로파일로 띄우려면: gradle bootRun -PbootProfile=dev // ('profile' 이 아니라 'bootProfile' 인 이유: 위 ext 블록이 profile='local' 을 이미 점유하고 있어 // findProperty('profile') 은 -P 지정 여부와 무관하게 항상 'local' 을 돌려준다.) def bootRunProfile = (project.findProperty('bootProfile') ?: 'local_rinjaemac').toString() args = ["--spring.profiles.active=" + bootRunProfile] } // annotation processor 출력 디렉토리는 IntelliJ/Gradle 통합이 자동 등록함. // 명시적으로 srcDir 추가하면 IntelliJ idea 모드에서 MapStruct가 같은 파일을 // 중복 생성하려다 javax.annotation.processing.FilerException 발생함. sourceSets { main { java { srcDir 'src/main/java' } } } configurations { annotationProcessor // WebLogic 배포 시 Tyrus WebSocket 필터(weblogic.websocket.tyrus.TyrusServletFilter)와 // 충돌 방지: WAR 에 번들된 Tomcat WsSci 가 javax.websocket.server.ServerContainer 속성을 // WsServerContainer 로 등록 → WebLogic Tyrus 필터가 TyrusServerContainer 로 캐스팅하다 실패. // 앱은 WebSocket 미사용이므로 Tomcat WebSocket 모듈 제외. all { exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-websocket' } } compileJava { options.annotationProcessorPath = configurations.annotationProcessor options.compilerArgs += ["-parameters"] options.encoding = 'UTF-8' } processResources { // exclude { details -> // details.file.name.startsWith('application-') && // details.file.name.endsWith('.yml') && // !(details.file.name in ['application-stage.yml', 'application-prod.yml']) // } } test { useJUnitPlatform { if (project.hasProperty('includeE2E') && project.includeE2E.toBoolean()) { includeTags 'e2e' } else { excludeTags 'e2e' } } enabled = true } // 로컬 전용 설정 파일. 배포 산출물(WAR)에 실리면 Actuator/SBA 설정이 그대로 노출되어 // 정보보호 점검에 불필요하게 걸린다. processResources 는 건드리지 않는다 // (bootRun 이 build/resources/main 을 그대로 쓰므로 로컬 기동이 깨진다). def localOnlyResources = ['**/application-local*.yml'] // 배포 산출물에서 로컬 전용 라이브러리를 걸러낸다. localOnlyLibPrefixes 는 파일 상단 정의. def excludeLocalOnlyLibs = { org.gradle.api.file.FileCollection cp -> cp.filter { f -> !localOnlyLibPrefixes.any { p -> f.name.startsWith(p) } } } bootWar { archiveFileName = "eapim-portal-boot.war" mainClass = 'com.eactive.apim.portal.PortalApplication' rootSpec.exclude(localOnlyResources) classpath = excludeLocalOnlyLibs(classpath) } war { archiveFileName = "eapim-portal.war" from('src/main/resources/jeus-web-dd.xml') { into 'WEB-INF' } from('src/main/resources/weblogic.xml') { into 'WEB-INF' } rootSpec.exclude(localOnlyResources) classpath = excludeLocalOnlyLibs(classpath) } task printSourceSets { doLast { sourceSets.each { srcSet -> println "SourceSet: ${srcSet.name}" println " Java srcDirs : ${srcSet.allJava.srcDirs}" println " Resources : ${srcSet.resources.srcDirs}" println " Output dir : ${srcSet.output.classesDirs.asPath}" } } } // CycloneDX SBOM -> xlsx 변환 (gradle sbomXlsx) apply from: "$projectDir/gradle/sbom-xlsx.gradle"