Kodėl priklausomybių saugumas tapo kritine problema
Kiekvieną dieną į mūsų projektus įtraukiame dešimtis, o kartais ir šimtus išorinių bibliotekų. Tai normalu – kam rašyti savo JWT autentifikacijos biblioteką, jei jau yra puikiai veikiančių sprendimų? Problema ta, kad kartu su patogumu gauname ir riziką.
Pagal 2023 metų statistiką, beveik 80% programinės įrangos pažeidžiamumų slypi būtent trečiųjų šalių komponentuose. Prisiminkite Log4Shell incidentą – viena pažeidžiama biblioteka sukėlė chaosą tūkstančiuose įmonių visame pasaulyje. O kiek tokių bibliotekų jūsų projekte? Tikrai daugiau nei viena.
OWASP Dependency-Check įrankis atsirado kaip atsakas į šią problemą. Tai nemokamas, atviro kodo sprendimas, kuris automatiškai skenuoja projekto priklausomybes ir tikrina jas pagal žinomų pažeidžiamumų duomenų bazes, tokias kaip National Vulnerability Database (NVD). Bet tikrasis šio įrankio potencialas atsiskleidžia tik tada, kai jį integruojame į CI/CD pipeline.
Kaip Dependency-Check veikia po gaubtu
Prieš pradedant konfigūruoti, verta suprasti, ką šis įrankis iš tikrųjų daro. Dependency-Check nėra magiškas juodas dėžutė – jis veikia pagal gana aiškią logiką.
Pirmiausia įrankis analizuoja jūsų projekto priklausomybes. Jis palaiko daugybę ekosistemų: Java (Maven, Gradle), .NET, Node.js, Python, Ruby ir kitas. Kiekvienai priklausomybei jis bando nustatyti tikslią versiją ir identifikatorius – CPE (Common Platform Enumeration) arba Package URL.
Tada prasideda tikrinimas. Dependency-Check lygina surastus komponentus su NVD duomenų baze, kuri reguliariai atnaujinama naujais CVE (Common Vulnerabilities and Exposures) įrašais. Jei randamas atitikimas, įrankis generuoja ataskaitą su detaliais pažeidžiamumo aprašymais, CVSS balais ir rekomendacijomis.
Svarbu suprasti, kad Dependency-Check naudoja kelias identifikavimo strategijas. Kartais jis analizuoja JAR failų metaduomenis, kartais – package.json failus, o kartais tiesiog tikrina failų hash reikšmes. Dėl to kartais gali būti false positive rezultatų, bet apie tai kalbėsime vėliau.
Pirmieji žingsniai: lokalus paleidimas ir konfigūracija
Prieš integruojant bet ką į CI/CD, rekomenduoju pirmiausia išbandyti įrankį lokaliai. Taip greičiau suprasite, kaip jis veikia ir ko tikėtis.
Jei dirbate su Maven projektu, paprasčiausias būdas:
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>8.4.0</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
<suppressionFile>dependency-check-suppressions.xml</suppressionFile>
</configuration>
</plugin>
Paleidžiate komandą mvn dependency-check:check ir po kelių minučių (pirmą kartą gali užtrukti ilgiau, nes parsisiunčia NVD duomenų bazę) gausite HTML ataskaitą su visais rastais pažeidžiamumais.
Gradle naudotojams procesas panašus:
plugins {
id 'org.owasp.dependencycheck' version '8.4.0'
}
dependencyCheck {
failBuildOnCVSS = 7
suppressionFile = 'dependency-check-suppressions.xml'
}
Parametras failBuildOnCVSS yra kritiškai svarbus – jis nustato, nuo kokio CVSS balo build’as turėtų būti laikomas nesėkmingu. Aš paprastai rekomenduoju pradėti nuo 7 (high severity), o vėliau, kai susitvarkysite su esamais pažeidžiamumais, galite sumažinti iki 4 ar net žemiau.
Integracija į Jenkins pipeline
Jenkins vis dar yra vienas populiariausių CI/CD įrankių, todėl pradėkime nuo jo. Yra du pagrindiniai būdai: naudoti Maven/Gradle plugin’ą arba tiesiogiai kviesti Dependency-Check CLI.
Štai pavyzdys su declarative pipeline:
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'mvn clean package'
}
}
stage('Dependency Check') {
steps {
sh 'mvn dependency-check:check'
}
}
}
post {
always {
dependencyCheckPublisher pattern: '**/dependency-check-report.xml'
}
}
}
Kad tai veiktų, reikia įdiegti „OWASP Dependency-Check Plugin” iš Jenkins plugin manager. Šis plugin’as leidžia gražiai vizualizuoti rezultatus Jenkins UI ir nustatyti build’o sėkmės kriterijus.
Vienas svarbus niuansas – NVD duomenų bazės parsisiuntimas. Pirmą kartą tai gali užtrukti 10-20 minučių, o kartais ir ilgiau, priklausomai nuo NVD API apkrovos. Todėl rekomenduoju:
- Naudoti cache mechanizmą duomenų bazei saugoti tarp build’ų
- Periodiškai atnaujinti duomenų bazę atskirame scheduled job’e
- Svarstyti mirror’o naudojimą, jei turite daug pipeline’ų
Praktinis patarimas: sukurkite atskirą Jenkins shared library funkciją dependency check’ui. Taip galėsite lengvai pakartotinai naudoti konfigūraciją skirtinguose projektuose.
GitLab CI ir GitHub Actions implementacijos
GitLab CI konfigūracija atrodo šiek tiek kitaip, bet principas tas pats. Štai .gitlab-ci.yml pavyzdys:
stages:
- build
- security
- deploy
dependency_check:
stage: security
image: maven:3.8-openjdk-17
script:
- mvn dependency-check:check
artifacts:
when: always
reports:
dependency_scanning: target/dependency-check-report.json
paths:
- target/dependency-check-report.html
cache:
key: dependency-check-db
paths:
- ~/.m2/repository/org/owasp/dependency-check-data/
allow_failure: false
only:
- merge_requests
- main
GitLab turi įmontuotą dependency scanning funkciją, bet ji veikia tik su Ultimate licencija. Dependency-Check yra puiki nemokama alternatyva.
GitHub Actions atveju galite naudoti jau paruoštus action’us iš marketplace:
name: Security Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Dependency-Check data
uses: actions/cache@v3
with:
path: ~/.m2/repository/org/owasp/dependency-check-data
key: ${{ runner.os }}-dependency-check-${{ hashFiles('**/pom.xml') }}
- name: Run Dependency-Check
run: mvn dependency-check:check
- name: Upload results
uses: actions/upload-artifact@v3
if: always()
with:
name: dependency-check-report
path: target/dependency-check-report.html
Svarbu paminėti, kad GitHub taip pat turi savo Dependabot, kuris automatiškai kuria PR su priklausomybių atnaujinimais. Dependency-Check ir Dependabot puikiai papildo vienas kitą – pirmasis randa problemas, antrasis padeda jas išspręsti.
False positive valdymas ir suppressions
Čia prasideda tikrasis darbas. Paleidę Dependency-Check pirmą kartą, greičiausiai gausite šimtus įspėjimų. Dalis jų bus tikri pažeidžiamumai, bet nemažai bus ir false positive.
False positive atsiranda dėl kelių priežasčių:
CPE atitikimo problemos: Jei turite biblioteką pavadinimu „commons-io”, Dependency-Check gali ją supainioti su visiškai kitu produktu, turinčiu panašų pavadinimą.
Netaikomi pažeidžiamumai: CVE gali būti susijęs su specifine funkcionalumo dalimi, kurios jūs nenaudojate. Arba jis taikomas tik Windows platformai, o jūs naudojate Linux.
Jau pataisyti pažeidžiamumai: Kartais vendor’iai patche’ina pažeidžiamumus nedarydami version bump, ypač backport’uose.
Suppressions failas yra jūsų ginklas prieš false positive. Štai pavyzdys:
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
<suppress>
<notes>
This CVE is related to Windows-specific functionality that we don't use
</notes>
<gav regex="true">^org\.example:my-library:.*$</gav>
<cve>CVE-2023-12345</cve>
</suppress>
<suppress until="2024-12-31">
<notes>
Temporary suppression - waiting for vendor fix in Q4 2024
</notes>
<gav regex="true">^com\.vendor:problematic-lib:2\.5\..*$</gav>
<cve>CVE-2023-67890</cve>
</suppress>
</suppressions>
Svarbus patarimas: visada rašykite notes su paaiškinimu, kodėl suppression’as buvo pridėtas. Po metų niekas neprisiminsite, kodėl konkretus CVE buvo ignoruojamas, ir bus sunku nuspręsti, ar jį dar reikia ignoruoti.
Naudokite until atributą laikiniems suppression’ams. Tai priverčia periodiškai peržiūrėti ir vertinti, ar problema vis dar aktuali.
Optimizavimas ir performance gerinimas
Dependency-Check gali būti lėtas. Labai lėtas. Ypač dideliuose projektuose su šimtais priklausomybių. Štai keletas būdų, kaip tai optimizuoti:
Naudokite cache: Tai svarbiausias dalykas. NVD duomenų bazė yra didelė (kelių GB), ir jos parsisiuntimas kiekvieną kartą yra laiko švaistymas. Visi modernus CI/CD įrankiai palaiko cache mechanizmus – naudokite juos.
Atnaujinkite duomenų bazę atskirai: Vietoj to, kad kiekvienas build’as atnaujintų duomenų bazę, sukurkite scheduled job’ą, kuris tai darys kartą per dieną ar savaitę:
update_nvd_database:
stage: maintenance
script:
- mvn dependency-check:update-only
cache:
key: dependency-check-db
paths:
- ~/.m2/repository/org/owasp/dependency-check-data/
policy: push
only:
- schedules
Išjunkite nereikalingus analyzer’ius: Jei jūsų projektas nenaudoja .NET, kam skenuoti .NET priklausomybes?
<configuration>
<assemblyAnalyzerEnabled>false</assemblyAnalyzerEnabled>
<nodeAnalyzerEnabled>false</nodeAnalyzerEnabled>
<retireJsAnalyzerEnabled>false</retireJsAnalyzerEnabled>
</configuration>
Paleiskite tik kritiniuose branch’uose: Nebūtina skenuoti kiekvieno feature branch’o. Pakanka skenuoti pull request’us į main/develop ir pačius šiuos branch’us.
Naudokite NVD API raktą: NVD dabar reikalauja API rakto greitesniam prieigai. Be jo, jūsų užklausos bus throttle’inamos. Registracija nemokama: https://nvd.nist.gov/developers/request-an-api-key
<configuration>
<nvdApiKey>${env.NVD_API_KEY}</nvdApiKey>
</configuration>
Ką daryti, kai randami pažeidžiamumai
Gerai, jūsų pipeline dabar veikia, ir jis ką tik rado 15 high severity pažeidžiamumų. Kas toliau?
Pirmiausia – nepanikoj. Ne visi pažeidžiamumai yra vienodai kritiniai jūsų kontekste. Štai mano rekomenduojamas workflow:
1. Triažas: Pereikite per kiekvieną pažeidžiamumą ir įvertinkite:
- Ar jūs naudojate pažeidžiamą funkcionalumą?
- Ar pažeidžiamumas eksploatuojamas jūsų aplinkoje?
- Koks yra realus rizikos lygis?
2. Prioritizavimas: Suskirstykite pažeidžiamumus pagal realią riziką:
- Critical: eksploatuojami production’e, turi public exploit’us
- High: potencialiai eksploatuojami, bet reikia specifinių sąlygų
- Medium: žemas eksploatavimo tikimybė arba ribotas poveikis
- Low: teoriniai arba reikalaujantys local access
3. Remediation: Kiekvienam pažeidžiamumui turite kelis pasirinkimus:
Atnaujinkite priklausomybę: Paprasčiausias ir geriausias sprendimas, jei yra pataisyta versija. Bet būkite atsargūs su major version upgrade’ais – jie gali sukelti breaking changes.
Pakeiskite biblioteką: Jei priklausomybė nebepalaiko arba pataisymas nebus greitai, ieškokite alternatyvų.
Pritaikykite workaround: Kartais galite apeiti pažeidžiamumą konfigūracijos pakeitimais arba nenaudodami specifinės funkcionalumo dalies.
Priimkite riziką: Jei pažeidžiamumas nėra eksploatuojamas jūsų kontekste, galite jį dokumentuoti ir priimti riziką (su suppression).
4. Dokumentavimas: Kiekvienas sprendimas turi būti dokumentuotas. Sukurkite JIRA ticket’us, pridėkite notes į suppression failą, atnaujinkite security dokumentaciją.
Monitoringas, ataskaitų valdymas ir nuolatinis tobulinimas
Dependency-Check integracija nėra „set and forget” sprendimas. Tai nuolatinis procesas, reikalaujantis dėmesio ir priežiūros.
Sukurkite dashboard’ą, kuris rodytų:
- Bendrą pažeidžiamumų skaičių per laiką
- Pažeidžiamumų pasiskirstymą pagal severity
- Vidutinį laiką nuo pažeidžiamumo aptikimo iki pataisymo
- Projektus su daugiausiai pažeidžiamumų
Jei naudojate Jenkins, galite integruoti rezultatus su InfluxDB ir Grafana. GitLab turi įmontuotą security dashboard. GitHub Security tab taip pat puikiai tinka.
Nustatykite reguliarius review procesus. Kas savaitę ar kas dvi savaites security team’as turėtų peržiūrėti:
- Naujus pažeidžiamumus
- Senus suppression’us (ar jie vis dar aktualūs?)
- Priklausomybes, kurios ilgai neatnaujintos
Automatizuokite pranešimus. Sukonfigūruokite Slack arba email notifikacijas kritiniams pažeidžiamumams:
post {
failure {
slackSend(
color: 'danger',
message: "Critical vulnerabilities found in ${env.JOB_NAME} - ${env.BUILD_URL}"
)
}
}
Integruokite su issue tracking sistemomis. Galite automatiškai kurti JIRA ticket’us naujiems pažeidžiamumams:
stage('Create Issues') {
steps {
script {
def report = readJSON file: 'target/dependency-check-report.json'
report.dependencies.each { dep ->
if (dep.vulnerabilities) {
dep.vulnerabilities.each { vuln ->
if (vuln.cvssv3?.baseScore > 7) {
jiraNewIssue(
site: 'YOUR_JIRA',
issue: [
fields: [
project: [key: 'SEC'],
summary: "Critical vulnerability: ${vuln.name}",
description: vuln.description,
issuetype: [name: 'Security Bug']
]
]
)
}
}
}
}
}
}
}
Realūs iššūkiai ir kaip juos spręsti
Per kelerius metus dirbdamas su Dependency-Check įvairiuose projektuose, susidūriau su daugybe iššūkių. Dalinuosi praktiniais sprendimais.
Problema: Build’ai tampa per lėti
Viename projekte dependency check pridėjo 15 minučių prie build laiko. Sprendimas buvo perkelti skenavimą į atskirą nightly build’ą ir paleisti tik delta skenavimą pull request’uose. Tai sumažino laiką iki 3 minučių, išlaikant saugumą.
Problema: Per daug false positive
Projekte su 200+ priklausomybių gavome 80 įspėjimų, iš kurių 60 buvo false positive. Užtruko dvi savaites juos visus išanalizuoti ir sukurti tinkamus suppression’us. Pamoka: pradėkite su aukštesniu CVSS slenksčiu (8-9) ir pamažu jį mažinkite.
Problema: NVD API rate limiting
Turėjome 20 projektų, visi jie bandė atnaujinti NVD duomenų bazę vienu metu. Sprendimas: centralizuotas NVD mirror’as su local cache, kurį visi projektai dalinasi.
Problema: Transitive dependencies
Pažeidžiamumas buvo ne jūsų tiesioginėje priklausomybėje, o jos priklausomybėje. Atnaujinimas negalimas, nes vendor’ius dar nepaleido naujos versijos. Sprendimas: dependency exclusion ir tiesioginis patched versijos įtraukimas:
<dependency>
<groupId>com.example</groupId>
<artifactId>main-lib</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>com.vulnerable</groupId>
<artifactId>vuln-lib</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.vulnerable</groupId>
<artifactId>vuln-lib</artifactId>
<version>2.0-patched</version>
</dependency>
Problema: Team’as ignoruoja įspėjimus
Tai organizacinis, ne techninis iššūkis. Sprendimas: security champion’ai kiekviename team’e, gamification (leaderboard kas greičiausiai taiso pažeidžiamumus), ir management support su aiškiomis policy.
Kur link eiti toliau: išplėstinės strategijos
Kai bazinė Dependency-Check integracija veikia sklandžiai, galite žengti toliau.
Software Composition Analysis (SCA) platformos: Dependency-Check yra puikus pradinis taškas, bet specializuotos SCA platformos kaip Snyk, WhiteSource ar Sonatype Nexus Lifecycle siūlo daugiau:
- Geresnis false positive valdymas
- License compliance tikrinimas
- Automatiniai remediation PR
- Geresnis transitive dependencies valdymas
Bet jos kainuoja. Dependency-Check išlieka puikiu nemokamu sprendimu mažesnėms organizacijoms.
Runtime protection: Dependency-Check tikrina statiškai, bet kas atsitinka runtime? Įrankiai kaip RASP (Runtime Application Self-Protection) gali aptikti eksploatavimo bandymus realiu laiku.
SBOM generavimas: Software Bill of Materials tampa standartu. Dependency-Check gali generuoti CycloneDX formatą:
<configuration>
<formats>
<format>HTML</format>
<format>JSON</format>
<format>XML</format>
<format>CYCLONEDX</format>
</formats>
</configuration>
SBOM tampa vis svarbesnis, ypač reguliuojamose industrijose ir vyriausybiniuose projektuose.
Container scanning: Jei naudojate Docker, nepamirškite skenuoti ir container image’ų. Dependency-Check gali analizuoti JAR failus container’iuose, bet specializuoti įrankiai kaip Trivy ar Clair yra efektyvesni.
Policy as Code: Apibrėžkite saugumo politikas kaip kodą naudojant Open Policy Agent (OPA):
package dependency_check
deny[msg] {
input.vulnerabilities[_].severity == "CRITICAL"
msg = "Critical vulnerabilities are not allowed"
}
deny[msg] {
input.vulnerabilities[_].cvssScore > 9
count(input.suppressions) == 0
msg = "CVSS > 9 vulnerabilities must have documented suppression"
}
Integruokite šias politikas į CI/CD, kad automatiškai užblokuotumėte build’us, kurie jų neatitinka.
Dependency-Check nėra tobulas, bet tai vienas geriausių nemokamų įrankių priklausomybių saugumui užtikrinti. Svarbiausias dalykas – pradėti jį naudoti. Net jei pradžioje bus sunku, net jei bus daug false positive, net jei build’ai sulėtės. Saugumas nėra vienkartinis projektas – tai nuolatinė kelionė. O Dependency-Check integracija į CI/CD pipeline yra vienas svarbiausių žingsnių toje kelionėje.
Pradėkite mažai – vienas projektas, aukštas CVSS slenkstis, daug suppression’ų. Pamažu tobulinkite procesą, mažinkite slenkstį, automatizuokite daugiau. Po metų atsigrįžę pamatysite, kiek daug pažangos padarėte. Ir svarbiausia – jūsų aplikacijos bus daug saugesnės.
