Skip to content

Commit

Permalink
Merge pull request #64 from ksoclabs/add-cyclonedx
Browse files Browse the repository at this point in the history
Add initial version of cyclonedx format
  • Loading branch information
mateuszdyminski authored Jun 30, 2023
2 parents 4613d61 + 72d0bd1 commit 50ac2e2
Show file tree
Hide file tree
Showing 13 changed files with 602 additions and 136 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ kbom generate [flags]
Optional flags include:

```plain
-f, --format string Format (json, yaml) (default "json")
-h, --help Help for generate
Flags:
-f, --format string Format (json, yaml, cyclonedx-json, cyclonedx-xml) (default "json")
-h, --help help for generate
-p, --out-path string Path to write KBOM file to. Works only with --output=file (default ".")
-o, --output string Output (stdout, file) (default "stdout")
--short Short - only include metadata, nodes, images and resources counters
Expand Down
271 changes: 271 additions & 0 deletions cmd/cyclonexdx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
package cmd

import (
"fmt"
"hash/fnv"
"time"

"github.com/CycloneDX/cyclonedx-go"
"github.com/google/uuid"
"github.com/ksoclabs/kbom/internal/model"
"github.com/mitchellh/hashstructure/v2"
)

const (
CdxPrefix = "cdx:"
KSOCPrefix = "ksoc:kbom:"

ClusterType = "cluster"
NodeType = "node"
ContainerType = "container"
)

func transformToCycloneDXBOM(kbom *model.KBOM) *cyclonedx.BOM { //nolint:funlen
cdxBOM := cyclonedx.NewBOM()

cdxBOM.SerialNumber = uuid.New().URN()
cdxBOM.Metadata = &cyclonedx.Metadata{
Timestamp: time.Now().Format(time.RFC3339),
Tools: &[]cyclonedx.Tool{
{
Vendor: kbom.GeneratedBy.Vendor,
Name: kbom.GeneratedBy.Name,
Version: kbom.GeneratedBy.Version,
},
},
Component: &cyclonedx.Component{
BOMRef: id(kbom.GeneratedBy),
Type: cyclonedx.ComponentTypeApplication,
Name: kbom.GeneratedBy.Name,
Hashes: &[]cyclonedx.Hash{
{
Algorithm: cyclonedx.HashAlgoSHA256,
Value: kbom.GeneratedBy.Commit,
},
},
Version: kbom.GeneratedBy.Version,
},
}

components := []cyclonedx.Component{}
clusterProperties := []cyclonedx.Property{
{
Name: CdxPrefix + "k8s:component:type",
Value: ClusterType,
},
{
Name: CdxPrefix + "k8s:component:name",
Value: kbom.Cluster.Name,
},
{
Name: KSOCPrefix + "k8s:cluster:nodes",
Value: fmt.Sprintf("%d", kbom.Cluster.NodesCount),
},
}

if kbom.Cluster.Location.Name != "" && kbom.Cluster.Location.Name != "unknown" {
clusterProperties = append(clusterProperties, cyclonedx.Property{
Name: KSOCPrefix + "k8s:cluster:location:name",
Value: kbom.Cluster.Location.Name,
})
}

if kbom.Cluster.Location.Region != "" {
clusterProperties = append(clusterProperties, cyclonedx.Property{
Name: KSOCPrefix + "k8s:cluster:location:region",
Value: kbom.Cluster.Location.Region,
})
}

if kbom.Cluster.Location.Zone != "" {
clusterProperties = append(clusterProperties, cyclonedx.Property{
Name: KSOCPrefix + "k8s:cluster:location:zone",
Value: kbom.Cluster.Location.Zone,
})
}

clusterComponent := cyclonedx.Component{
BOMRef: id(kbom.Cluster),
Type: cyclonedx.ComponentTypePlatform,
Name: "cluster",
Version: kbom.Cluster.K8sVersion,
Properties: &clusterProperties,
}

components = append(components, clusterComponent)

for i := range kbom.Cluster.Nodes {
n := kbom.Cluster.Nodes[i]
components = append(components, cyclonedx.Component{
BOMRef: id(n),
Type: cyclonedx.ComponentTypePlatform,
Name: n.Name,
Properties: &[]cyclonedx.Property{
{
Name: CdxPrefix + "k8s:component:type",
Value: NodeType,
},
{
Name: CdxPrefix + "k8s:component:name",
Value: n.Name,
},
{
Name: KSOCPrefix + "k8s:node:osImage",
Value: n.OsImage,
},
{
Name: KSOCPrefix + "k8s:node:arch",
Value: n.Architecture,
},
{
Name: KSOCPrefix + "k8s:node:kernel",
Value: n.KernelVersion,
},
{
Name: KSOCPrefix + "k8s:node:bootId",
Value: n.BootID,
},
{
Name: KSOCPrefix + "k8s:node:type",
Value: n.Type,
},
{
Name: KSOCPrefix + "k8s:node:operatingSystem",
Value: n.OperatingSystem,
},
{
Name: KSOCPrefix + "k8s:node:machineId",
Value: n.MachineID,
},
{
Name: KSOCPrefix + "k8s:node:hostname",
Value: n.Hostname,
},
{
Name: KSOCPrefix + "k8s:node:containerRuntimeVersion",
Value: n.ContainerRuntimeVersion,
},
{
Name: KSOCPrefix + "k8s:node:kubeletVersion",
Value: n.KubeletVersion,
},
{
Name: KSOCPrefix + "k8s:node:kubeProxyVersion",
Value: n.KubeProxyVersion,
},
{
Name: KSOCPrefix + "k8s:node:capacity:cpu",
Value: n.Capacity.CPU,
},
{
Name: KSOCPrefix + "k8s:node:capacity:memory",
Value: n.Capacity.Memory,
},
{
Name: KSOCPrefix + "k8s:node:capacity:pods",
Value: n.Capacity.Pods,
},
{
Name: KSOCPrefix + "k8s:node:capacity:ephemeralStorage",
Value: n.Capacity.EphemeralStorage,
},
},
})
}

for _, img := range kbom.Cluster.Components.Images {
container := cyclonedx.Component{
BOMRef: img.PkgID(),
Type: cyclonedx.ComponentTypeContainer,
Name: img.Name,
Version: img.Digest,
PackageURL: img.PkgID(),
Properties: &[]cyclonedx.Property{
{
Name: CdxPrefix + "k8s:component:type",
Value: ContainerType,
},
{
Name: CdxPrefix + "k8s:component:name",
Value: img.Name,
},
{
Name: KSOCPrefix + "pkg:type",
Value: "oci",
},
{
Name: KSOCPrefix + "pkg:name",
Value: img.Name,
},
{
Name: KSOCPrefix + "pkg:version",
Value: img.Version,
},
{
Name: KSOCPrefix + "pkg:digest",
Value: img.Digest,
},
},
}

components = append(components, container)
}

for _, resList := range kbom.Cluster.Components.Resources {
for _, res := range resList.Resources {
properties := []cyclonedx.Property{
{
Name: CdxPrefix + "k8s:component:type",
Value: resList.Kind,
},
{
Name: CdxPrefix + "k8s:component:name",
Value: res.Name,
},
{
Name: KSOCPrefix + "k8s:component:apiVersion",
Value: resList.APIVersion,
},
}

if resList.Namespaced {
properties = append(properties, cyclonedx.Property{
Name: KSOCPrefix + "k8s:component:namespace",
Value: res.Namespace,
})
}

resource := cyclonedx.Component{
BOMRef: id(res),
Type: cyclonedx.ComponentTypeApplication, // TODO: this is not perfect but we don't have a better option
Name: res.Name,
Version: res.APIVersion,
Properties: &properties,
}

components = append(components, resource)
}
}

cdxBOM.Components = &components

// TODO: add relationships and dependencies

return cdxBOM
}

func id(obj interface{}) string {
f, err := hashstructure.Hash(obj, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
SlicesAsSets: true,
Hasher: fnv.New64(),
})

// this should never happen, but if it does, we don't want to crash - use empty string
if err != nil {
fmt.Printf("failed to hash object: %v", err)
return ""
}

return fmt.Sprintf("%016x", f)
}
52 changes: 52 additions & 0 deletions cmd/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cmd

import "fmt"

type Format struct {
Name string
FileExtension string
}

var JSONFormat = Format{
Name: "json",
FileExtension: "json",
}

var YAMLFormat = Format{
Name: "yaml",
FileExtension: "yaml",
}

var CycloneDXJsonFormat = Format{
Name: "cyclonedx-json",
FileExtension: "json",
}

var CycloneDXXMLFormat = Format{
Name: "cyclonedx-xml",
FileExtension: "xml",
}

func formatNames() []string {
return []string{
JSONFormat.Name,
YAMLFormat.Name,
CycloneDXJsonFormat.Name,
CycloneDXXMLFormat.Name,
}
}

func formatFromName(name string) (Format, error) {
switch name {
case JSONFormat.Name:
return JSONFormat, nil
case YAMLFormat.Name:
return YAMLFormat, nil
case CycloneDXJsonFormat.Name:
return CycloneDXJsonFormat, nil
case CycloneDXXMLFormat.Name:
return CycloneDXXMLFormat, nil
default:
return Format{}, fmt.Errorf("format %q is not supported", name)
}
}
Loading

0 comments on commit 50ac2e2

Please sign in to comment.