feat(report): add cyclonedx format (#1543)
This commit is contained in:
@@ -2,24 +2,30 @@ package reporter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/future-architect/vuls/models"
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/future-architect/vuls/models"
|
||||
"github.com/future-architect/vuls/reporter/sbom"
|
||||
)
|
||||
|
||||
// LocalFileWriter writes results to a local file.
|
||||
type LocalFileWriter struct {
|
||||
CurrentDir string
|
||||
DiffPlus bool
|
||||
DiffMinus bool
|
||||
FormatJSON bool
|
||||
FormatCsv bool
|
||||
FormatFullText bool
|
||||
FormatOneLineText bool
|
||||
FormatList bool
|
||||
Gzip bool
|
||||
CurrentDir string
|
||||
DiffPlus bool
|
||||
DiffMinus bool
|
||||
FormatJSON bool
|
||||
FormatCsv bool
|
||||
FormatFullText bool
|
||||
FormatOneLineText bool
|
||||
FormatList bool
|
||||
FormatCycloneDXJSON bool
|
||||
FormatCycloneDXXML bool
|
||||
Gzip bool
|
||||
}
|
||||
|
||||
func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) {
|
||||
@@ -86,6 +92,28 @@ func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if w.FormatCycloneDXJSON {
|
||||
bs, err := sbom.GenerateCycloneDX(cyclonedx.BOMFileFormatJSON, r)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("Failed to generate CycloneDX JSON. err: %w", err)
|
||||
}
|
||||
p := fmt.Sprintf("%s_cyclonedx.json", path)
|
||||
if err := w.writeFile(p, bs, 0600); err != nil {
|
||||
return xerrors.Errorf("Failed to write CycloneDX JSON. path: %s, err: %w", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
if w.FormatCycloneDXXML {
|
||||
bs, err := sbom.GenerateCycloneDX(cyclonedx.BOMFileFormatXML, r)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("Failed to generate CycloneDX XML. err: %w", err)
|
||||
}
|
||||
p := fmt.Sprintf("%s_cyclonedx.xml", path)
|
||||
if err := w.writeFile(p, bs, 0600); err != nil {
|
||||
return xerrors.Errorf("Failed to write CycloneDX XML. path: %s, err: %w", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
510
reporter/sbom/cyclonedx.go
Normal file
510
reporter/sbom/cyclonedx.go
Normal file
@@ -0,0 +1,510 @@
|
||||
package sbom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cdx "github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/package-url/packageurl-go"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/future-architect/vuls/constant"
|
||||
"github.com/future-architect/vuls/models"
|
||||
)
|
||||
|
||||
func GenerateCycloneDX(format cdx.BOMFileFormat, r models.ScanResult) ([]byte, error) {
|
||||
bom := cdx.NewBOM()
|
||||
bom.SerialNumber = uuid.New().URN()
|
||||
bom.Metadata = cdxMetadata(r)
|
||||
bom.Components, bom.Dependencies, bom.Vulnerabilities = cdxComponents(r, bom.Metadata.Component.BOMRef)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
enc := cdx.NewBOMEncoder(buf, format)
|
||||
enc.SetPretty(true)
|
||||
if err := enc.Encode(bom); err != nil {
|
||||
return nil, xerrors.Errorf("Failed to encode CycloneDX. err: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func cdxMetadata(result models.ScanResult) *cdx.Metadata {
|
||||
metadata := cdx.Metadata{
|
||||
Timestamp: result.ReportedAt.Format(time.RFC3339),
|
||||
Tools: &[]cdx.Tool{
|
||||
{
|
||||
Vendor: "future-architect",
|
||||
Name: "vuls",
|
||||
Version: fmt.Sprintf("%s-%s", result.ReportedVersion, result.ReportedRevision),
|
||||
},
|
||||
},
|
||||
Component: &cdx.Component{
|
||||
BOMRef: uuid.NewString(),
|
||||
Type: cdx.ComponentTypeOS,
|
||||
Name: result.ServerName,
|
||||
},
|
||||
}
|
||||
return &metadata
|
||||
}
|
||||
|
||||
func cdxComponents(result models.ScanResult, metaBomRef string) (*[]cdx.Component, *[]cdx.Dependency, *[]cdx.Vulnerability) {
|
||||
var components []cdx.Component
|
||||
bomRefs := map[string][]string{}
|
||||
|
||||
ospkgToPURL := map[string]string{}
|
||||
if ospkgComps := ospkgToCdxComponents(result.Family, result.Release, result.RunningKernel, result.Packages, result.SrcPackages, ospkgToPURL); ospkgComps != nil {
|
||||
bomRefs[metaBomRef] = append(bomRefs[metaBomRef], ospkgComps[0].BOMRef)
|
||||
for _, comp := range ospkgComps[1:] {
|
||||
bomRefs[ospkgComps[0].BOMRef] = append(bomRefs[ospkgComps[0].BOMRef], comp.BOMRef)
|
||||
}
|
||||
components = append(components, ospkgComps...)
|
||||
}
|
||||
|
||||
if cpeComps := cpeToCdxComponents(result.ScannedCves); cpeComps != nil {
|
||||
bomRefs[metaBomRef] = append(bomRefs[metaBomRef], cpeComps[0].BOMRef)
|
||||
for _, comp := range cpeComps[1:] {
|
||||
bomRefs[cpeComps[0].BOMRef] = append(bomRefs[cpeComps[0].BOMRef], comp.BOMRef)
|
||||
}
|
||||
components = append(components, cpeComps...)
|
||||
}
|
||||
|
||||
libpkgToPURL := map[string]map[string]string{}
|
||||
for _, libscanner := range result.LibraryScanners {
|
||||
libpkgToPURL[libscanner.LockfilePath] = map[string]string{}
|
||||
|
||||
libpkgComps := libpkgToCdxComponents(libscanner, libpkgToPURL)
|
||||
bomRefs[metaBomRef] = append(bomRefs[metaBomRef], libpkgComps[0].BOMRef)
|
||||
for _, comp := range libpkgComps[1:] {
|
||||
bomRefs[libpkgComps[0].BOMRef] = append(bomRefs[libpkgComps[0].BOMRef], comp.BOMRef)
|
||||
}
|
||||
components = append(components, libpkgComps...)
|
||||
}
|
||||
|
||||
wppkgToPURL := map[string]string{}
|
||||
if wppkgComps := wppkgToCdxComponents(result.WordPressPackages, wppkgToPURL); wppkgComps != nil {
|
||||
bomRefs[metaBomRef] = append(bomRefs[metaBomRef], wppkgComps[0].BOMRef)
|
||||
for _, comp := range wppkgComps[1:] {
|
||||
bomRefs[wppkgComps[0].BOMRef] = append(bomRefs[wppkgComps[0].BOMRef], comp.BOMRef)
|
||||
}
|
||||
components = append(components, wppkgComps...)
|
||||
}
|
||||
|
||||
return &components, cdxDependencies(bomRefs), cdxVulnerabilities(result, ospkgToPURL, libpkgToPURL, wppkgToPURL)
|
||||
}
|
||||
|
||||
func osToCdxComponent(family, release, runningKernelRelease, runningKernelVersion string) cdx.Component {
|
||||
props := []cdx.Property{
|
||||
{
|
||||
Name: "future-architect:vuls:Type",
|
||||
Value: "Package",
|
||||
},
|
||||
}
|
||||
if runningKernelRelease != "" {
|
||||
props = append(props, cdx.Property{
|
||||
Name: "RunningKernelRelease",
|
||||
Value: runningKernelRelease,
|
||||
})
|
||||
}
|
||||
if runningKernelVersion != "" {
|
||||
props = append(props, cdx.Property{
|
||||
Name: "RunningKernelVersion",
|
||||
Value: runningKernelVersion,
|
||||
})
|
||||
}
|
||||
return cdx.Component{
|
||||
BOMRef: uuid.NewString(),
|
||||
Type: cdx.ComponentTypeOS,
|
||||
Name: family,
|
||||
Version: release,
|
||||
Properties: &props,
|
||||
}
|
||||
}
|
||||
|
||||
func ospkgToCdxComponents(family, release string, runningKernel models.Kernel, binpkgs models.Packages, srcpkgs models.SrcPackages, ospkgToPURL map[string]string) []cdx.Component {
|
||||
if family == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
components := []cdx.Component{
|
||||
osToCdxComponent(family, release, runningKernel.Release, runningKernel.Version),
|
||||
}
|
||||
|
||||
if len(binpkgs) == 0 {
|
||||
return components
|
||||
}
|
||||
|
||||
type srcpkg struct {
|
||||
name string
|
||||
version string
|
||||
arch string
|
||||
}
|
||||
binToSrc := map[string]srcpkg{}
|
||||
for _, pack := range srcpkgs {
|
||||
for _, binpkg := range pack.BinaryNames {
|
||||
binToSrc[binpkg] = srcpkg{
|
||||
name: pack.Name,
|
||||
version: pack.Version,
|
||||
arch: pack.Arch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, pack := range binpkgs {
|
||||
var props []cdx.Property
|
||||
if p, ok := binToSrc[pack.Name]; ok {
|
||||
if p.name != "" {
|
||||
props = append(props, cdx.Property{
|
||||
Name: "future-architect:vuls:SrcName",
|
||||
Value: p.name,
|
||||
})
|
||||
}
|
||||
if p.version != "" {
|
||||
props = append(props, cdx.Property{
|
||||
Name: "future-architect:vuls:SrcVersion",
|
||||
Value: p.version,
|
||||
})
|
||||
}
|
||||
if p.arch != "" {
|
||||
props = append(props, cdx.Property{
|
||||
Name: "future-architect:vuls:SrcArch",
|
||||
Value: p.arch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
purl := toPkgPURL(family, release, pack.Name, pack.Version, pack.Release, pack.Arch, pack.Repository)
|
||||
components = append(components, cdx.Component{
|
||||
BOMRef: purl,
|
||||
Type: cdx.ComponentTypeLibrary,
|
||||
Name: pack.Name,
|
||||
Version: pack.Version,
|
||||
PackageURL: purl,
|
||||
Properties: &props,
|
||||
})
|
||||
|
||||
ospkgToPURL[pack.Name] = purl
|
||||
}
|
||||
return components
|
||||
}
|
||||
|
||||
func cpeToCdxComponents(scannedCves models.VulnInfos) []cdx.Component {
|
||||
cpes := map[string]struct{}{}
|
||||
for _, cve := range scannedCves {
|
||||
for _, cpe := range cve.CpeURIs {
|
||||
cpes[cpe] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(cpes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
components := []cdx.Component{
|
||||
{
|
||||
BOMRef: uuid.NewString(),
|
||||
Type: cdx.ComponentTypeApplication,
|
||||
Name: "CPEs",
|
||||
Properties: &[]cdx.Property{
|
||||
{
|
||||
Name: "future-architect:vuls:Type",
|
||||
Value: "CPE",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for cpe := range cpes {
|
||||
components = append(components, cdx.Component{
|
||||
BOMRef: cpe,
|
||||
Type: cdx.ComponentTypeLibrary,
|
||||
Name: cpe,
|
||||
CPE: cpe,
|
||||
})
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
func libpkgToCdxComponents(libscanner models.LibraryScanner, libpkgToPURL map[string]map[string]string) []cdx.Component {
|
||||
components := []cdx.Component{
|
||||
{
|
||||
BOMRef: uuid.NewString(),
|
||||
Type: cdx.ComponentTypeApplication,
|
||||
Name: libscanner.LockfilePath,
|
||||
Properties: &[]cdx.Property{
|
||||
{
|
||||
Name: "future-architect:vuls:Type",
|
||||
Value: libscanner.Type,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, lib := range libscanner.Libs {
|
||||
purl := packageurl.NewPackageURL(libscanner.Type, "", lib.Name, lib.Version, packageurl.Qualifiers{{Key: "file_path", Value: libscanner.LockfilePath}}, "").ToString()
|
||||
components = append(components, cdx.Component{
|
||||
BOMRef: purl,
|
||||
Type: cdx.ComponentTypeLibrary,
|
||||
Name: lib.Name,
|
||||
Version: lib.Version,
|
||||
PackageURL: purl,
|
||||
})
|
||||
|
||||
libpkgToPURL[libscanner.LockfilePath][lib.Name] = purl
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
func wppkgToCdxComponents(wppkgs models.WordPressPackages, wppkgToPURL map[string]string) []cdx.Component {
|
||||
if len(wppkgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
components := []cdx.Component{
|
||||
{
|
||||
BOMRef: uuid.NewString(),
|
||||
Type: cdx.ComponentTypeApplication,
|
||||
Name: "wordpress",
|
||||
Properties: &[]cdx.Property{
|
||||
{
|
||||
Name: "future-architect:vuls:Type",
|
||||
Value: "WordPress",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, wppkg := range wppkgs {
|
||||
purl := packageurl.NewPackageURL("wordpress", wppkg.Type, wppkg.Name, wppkg.Version, packageurl.Qualifiers{{Key: "status", Value: wppkg.Status}}, "").ToString()
|
||||
components = append(components, cdx.Component{
|
||||
BOMRef: purl,
|
||||
Type: cdx.ComponentTypeLibrary,
|
||||
Name: wppkg.Name,
|
||||
Version: wppkg.Version,
|
||||
PackageURL: purl,
|
||||
})
|
||||
|
||||
wppkgToPURL[wppkg.Name] = purl
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
func cdxDependencies(bomRefs map[string][]string) *[]cdx.Dependency {
|
||||
dependencies := make([]cdx.Dependency, 0, len(bomRefs))
|
||||
for ref, depRefs := range bomRefs {
|
||||
ds := depRefs
|
||||
dependencies = append(dependencies, cdx.Dependency{
|
||||
Ref: ref,
|
||||
Dependencies: &ds,
|
||||
})
|
||||
}
|
||||
return &dependencies
|
||||
}
|
||||
|
||||
func toPkgPURL(osFamily, osVersion, packName, packVersion, packRelease, packArch, packRepository string) string {
|
||||
var purlType string
|
||||
switch osFamily {
|
||||
case constant.Alma, constant.Amazon, constant.CentOS, constant.Fedora, constant.OpenSUSE, constant.OpenSUSELeap, constant.Oracle, constant.RedHat, constant.Rocky, constant.SUSEEnterpriseDesktop, constant.SUSEEnterpriseServer:
|
||||
purlType = "rpm"
|
||||
case constant.Alpine:
|
||||
purlType = "apk"
|
||||
case constant.Debian, constant.Raspbian, constant.Ubuntu:
|
||||
purlType = "deb"
|
||||
case constant.FreeBSD:
|
||||
purlType = "pkg"
|
||||
case constant.Windows:
|
||||
purlType = "win"
|
||||
case constant.ServerTypePseudo:
|
||||
purlType = "pseudo"
|
||||
default:
|
||||
purlType = "unknown"
|
||||
}
|
||||
|
||||
version := packVersion
|
||||
if packRelease != "" {
|
||||
version = fmt.Sprintf("%s-%s", packVersion, packRelease)
|
||||
}
|
||||
|
||||
var qualifiers packageurl.Qualifiers
|
||||
if osVersion != "" {
|
||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||
Key: "distro",
|
||||
Value: osVersion,
|
||||
})
|
||||
}
|
||||
if packArch != "" {
|
||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||
Key: "arch",
|
||||
Value: packArch,
|
||||
})
|
||||
}
|
||||
if packRepository != "" {
|
||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||
Key: "repo",
|
||||
Value: packRepository,
|
||||
})
|
||||
}
|
||||
|
||||
return packageurl.NewPackageURL(purlType, osFamily, packName, version, qualifiers, "").ToString()
|
||||
}
|
||||
|
||||
func cdxVulnerabilities(result models.ScanResult, ospkgToPURL map[string]string, libpkgToPURL map[string]map[string]string, wppkgToPURL map[string]string) *[]cdx.Vulnerability {
|
||||
vulnerabilities := make([]cdx.Vulnerability, 0, len(result.ScannedCves))
|
||||
for _, cve := range result.ScannedCves {
|
||||
vulnerabilities = append(vulnerabilities, cdx.Vulnerability{
|
||||
ID: cve.CveID,
|
||||
Ratings: cdxRatings(cve.CveContents),
|
||||
CWEs: cdxCWEs(cve.CveContents),
|
||||
Description: cdxDescription(cve.CveContents),
|
||||
Advisories: cdxAdvisories(cve.CveContents),
|
||||
Affects: cdxAffects(cve, ospkgToPURL, libpkgToPURL, wppkgToPURL),
|
||||
})
|
||||
}
|
||||
return &vulnerabilities
|
||||
}
|
||||
|
||||
func cdxRatings(cveContents models.CveContents) *[]cdx.VulnerabilityRating {
|
||||
var ratings []cdx.VulnerabilityRating
|
||||
for _, contents := range cveContents {
|
||||
for _, content := range contents {
|
||||
if content.Cvss2Score != 0 || content.Cvss2Vector != "" || content.Cvss2Severity != "" {
|
||||
ratings = append(ratings, cdxCVSS2Rating(string(content.Type), content.Cvss2Vector, content.Cvss2Score, content.Cvss2Severity))
|
||||
}
|
||||
if content.Cvss3Score != 0 || content.Cvss3Vector != "" || content.Cvss3Severity != "" {
|
||||
ratings = append(ratings, cdxCVSS3Rating(string(content.Type), content.Cvss3Vector, content.Cvss3Score, content.Cvss3Severity))
|
||||
}
|
||||
}
|
||||
}
|
||||
return &ratings
|
||||
}
|
||||
|
||||
func cdxCVSS2Rating(source, vector string, score float64, severity string) cdx.VulnerabilityRating {
|
||||
r := cdx.VulnerabilityRating{
|
||||
Source: &cdx.Source{Name: source},
|
||||
Method: cdx.ScoringMethodCVSSv2,
|
||||
Vector: vector,
|
||||
}
|
||||
if score != 0 {
|
||||
r.Score = &score
|
||||
}
|
||||
switch strings.ToLower(severity) {
|
||||
case "high":
|
||||
r.Severity = cdx.SeverityHigh
|
||||
case "medium":
|
||||
r.Severity = cdx.SeverityMedium
|
||||
case "low":
|
||||
r.Severity = cdx.SeverityLow
|
||||
default:
|
||||
r.Severity = cdx.SeverityUnknown
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func cdxCVSS3Rating(source, vector string, score float64, severity string) cdx.VulnerabilityRating {
|
||||
r := cdx.VulnerabilityRating{
|
||||
Source: &cdx.Source{Name: source},
|
||||
Method: cdx.ScoringMethodCVSSv3,
|
||||
Vector: vector,
|
||||
}
|
||||
if strings.HasPrefix(vector, "CVSS:3.1") {
|
||||
r.Method = cdx.ScoringMethodCVSSv31
|
||||
}
|
||||
if score != 0 {
|
||||
r.Score = &score
|
||||
}
|
||||
switch strings.ToLower(severity) {
|
||||
case "critical":
|
||||
r.Severity = cdx.SeverityCritical
|
||||
case "high":
|
||||
r.Severity = cdx.SeverityHigh
|
||||
case "medium":
|
||||
r.Severity = cdx.SeverityMedium
|
||||
case "low":
|
||||
r.Severity = cdx.SeverityLow
|
||||
case "none":
|
||||
r.Severity = cdx.SeverityNone
|
||||
default:
|
||||
r.Severity = cdx.SeverityUnknown
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func cdxAffects(cve models.VulnInfo, ospkgToPURL map[string]string, libpkgToPURL map[string]map[string]string, wppkgToPURL map[string]string) *[]cdx.Affects {
|
||||
affects := make([]cdx.Affects, 0, len(cve.AffectedPackages)+len(cve.CpeURIs)+len(cve.LibraryFixedIns)+len(cve.WpPackageFixStats))
|
||||
|
||||
for _, p := range cve.AffectedPackages {
|
||||
affects = append(affects, cdx.Affects{
|
||||
Ref: ospkgToPURL[p.Name],
|
||||
})
|
||||
}
|
||||
for _, cpe := range cve.CpeURIs {
|
||||
affects = append(affects, cdx.Affects{
|
||||
Ref: cpe,
|
||||
})
|
||||
}
|
||||
for _, lib := range cve.LibraryFixedIns {
|
||||
affects = append(affects, cdx.Affects{
|
||||
Ref: libpkgToPURL[lib.Path][lib.Name],
|
||||
})
|
||||
|
||||
}
|
||||
for _, wppack := range cve.WpPackageFixStats {
|
||||
affects = append(affects, cdx.Affects{
|
||||
Ref: wppkgToPURL[wppack.Name],
|
||||
})
|
||||
}
|
||||
|
||||
return &affects
|
||||
}
|
||||
|
||||
func cdxCWEs(cveContents models.CveContents) *[]int {
|
||||
m := map[int]struct{}{}
|
||||
for _, contents := range cveContents {
|
||||
for _, content := range contents {
|
||||
for _, cweID := range content.CweIDs {
|
||||
if !strings.HasPrefix(cweID, "CWE-") {
|
||||
continue
|
||||
}
|
||||
i, err := strconv.Atoi(strings.TrimPrefix(cweID, "CWE-"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
m[i] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
cweIDs := maps.Keys(m)
|
||||
return &cweIDs
|
||||
}
|
||||
|
||||
func cdxDescription(cveContents models.CveContents) string {
|
||||
if contents, ok := cveContents[models.Nvd]; ok {
|
||||
return contents[0].Summary
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func cdxAdvisories(cveContents models.CveContents) *[]cdx.Advisory {
|
||||
urls := map[string]struct{}{}
|
||||
for _, contents := range cveContents {
|
||||
for _, content := range contents {
|
||||
if content.SourceLink != "" {
|
||||
urls[content.SourceLink] = struct{}{}
|
||||
}
|
||||
for _, r := range content.References {
|
||||
urls[r.Link] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
advisories := make([]cdx.Advisory, 0, len(urls))
|
||||
for u := range urls {
|
||||
advisories = append(advisories, cdx.Advisory{
|
||||
URL: u,
|
||||
})
|
||||
}
|
||||
return &advisories
|
||||
}
|
||||
Reference in New Issue
Block a user