Skip to content

Commit 3ed34da

Browse files
alistair-mcleanAlistair McLean
andauthored
Added 5 missing fields to match CycloneDX 1.6 spec: (#256)
* Added 5 missing fields to match CycloneDX 1.6 spec: - Name (string) - identifier for the data - Description (string) - description of data content/usage - Governance (*DataGovernance) - data governance metadata - Source (*[]string) - array of URIs/URLs/BOM-Links where data originates - Destination (*[]string) - array of URIs/URLs/BOM-Links where data is sent Updated service struct XML tag & implemented custom XML marshalling Updated test files to conform to the new standard. Signed-off-by: Alistair McLean <alistair.mclean@netrise.io> * Moving conversion logic, and handling cases for specs <1.6 Signed-off-by: Alistair McLean <alistair.mclean@netrise.io> * go fmt Signed-off-by: Alistair McLean <alistair.mclean@netrise.io> --------- Fixes #208 --------- Signed-off-by: Alistair McLean <alistair.mclean@netrise.io> Co-authored-by: Alistair McLean <alistair.mclean@netrise.io>
1 parent 2270566 commit 3ed34da

13 files changed

+493
-21
lines changed

convert.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,24 @@ func convertCompositions(comps *[]Composition, specVersion SpecVersion) {
237237
}
238238
}
239239

240+
// convertDataClassifications modifies a DataClassification slice such that it adheres to a given SpecVersion.
241+
func convertDataClassifications(dataClassifications *[]DataClassification, specVersion SpecVersion) {
242+
if dataClassifications == nil {
243+
return
244+
}
245+
246+
// v1.6 introduced Name, Description, Governance, Source, and Destination fields
247+
if specVersion < SpecVersion1_6 {
248+
for i := range *dataClassifications {
249+
(*dataClassifications)[i].Name = ""
250+
(*dataClassifications)[i].Description = ""
251+
(*dataClassifications)[i].Governance = nil
252+
(*dataClassifications)[i].Source = nil
253+
(*dataClassifications)[i].Destination = nil
254+
}
255+
}
256+
}
257+
240258
// convertExternalReferences modifies an ExternalReference slice such that it adheres to a given SpecVersion.
241259
func convertExternalReferences(extRefs *[]ExternalReference, specVersion SpecVersion) {
242260
if extRefs == nil {
@@ -466,6 +484,10 @@ func serviceConverter(specVersion SpecVersion) func(*Service) {
466484
s.TrustZone = ""
467485
}
468486

487+
if specVersion < SpecVersion1_6 {
488+
convertDataClassifications(s.Data, specVersion)
489+
}
490+
469491
convertOrganizationalEntity(s.Provider, specVersion)
470492
convertExternalReferences(s.ExternalReferences, specVersion)
471493
}

cyclonedx.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,8 +522,13 @@ type SecuredBy struct {
522522
}
523523

524524
type DataClassification struct {
525-
Flow DataFlow `json:"flow" xml:"flow,attr"`
526-
Classification string `json:"classification" xml:",chardata"`
525+
Flow DataFlow `json:"flow" xml:"flow,attr"`
526+
Classification string `json:"classification" xml:",chardata"`
527+
Name string `json:"name,omitempty" xml:"name,attr,omitempty"`
528+
Description string `json:"description,omitempty" xml:"description,attr,omitempty"`
529+
Governance *DataGovernance `json:"governance,omitempty" xml:"governance,omitempty"`
530+
Source *[]string `json:"source,omitempty" xml:"source>url,omitempty"`
531+
Destination *[]string `json:"destination,omitempty" xml:"destination>url,omitempty"`
527532
}
528533

529534
type DataFlow string
@@ -1296,7 +1301,7 @@ type Service struct {
12961301
Endpoints *[]string `json:"endpoints,omitempty" xml:"endpoints>endpoint,omitempty"`
12971302
Authenticated *bool `json:"authenticated,omitempty" xml:"authenticated,omitempty"`
12981303
CrossesTrustBoundary *bool `json:"x-trust-boundary,omitempty" xml:"x-trust-boundary,omitempty"`
1299-
Data *[]DataClassification `json:"data,omitempty" xml:"data>classification,omitempty"`
1304+
Data *[]DataClassification `json:"data,omitempty" xml:"data>dataflow,omitempty"`
13001305
Licenses *Licenses `json:"licenses,omitempty" xml:"licenses,omitempty"`
13011306
ExternalReferences *[]ExternalReference `json:"externalReferences,omitempty" xml:"externalReferences>reference,omitempty"`
13021307
Properties *[]Property `json:"properties,omitempty" xml:"properties>property,omitempty"`

cyclonedx_xml.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,182 @@ func (ev *Evidence) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error {
542542
return nil
543543
}
544544

545+
// MarshalXML implements custom XML marshaling for DataClassification to support the v1.6 dataflow format
546+
func (dc DataClassification) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
547+
start.Name.Local = "dataflow"
548+
549+
// Add name and description as attributes if present
550+
if dc.Name != "" {
551+
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "name"}, Value: dc.Name})
552+
}
553+
if dc.Description != "" {
554+
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "description"}, Value: dc.Description})
555+
}
556+
557+
if err := e.EncodeToken(start); err != nil {
558+
return err
559+
}
560+
561+
// Encode classification element with flow attribute and text content
562+
if dc.Classification != "" || dc.Flow != "" {
563+
classStart := xml.StartElement{Name: xml.Name{Local: "classification"}}
564+
if dc.Flow != "" {
565+
classStart.Attr = append(classStart.Attr, xml.Attr{Name: xml.Name{Local: "flow"}, Value: string(dc.Flow)})
566+
}
567+
if err := e.EncodeToken(classStart); err != nil {
568+
return err
569+
}
570+
if dc.Classification != "" {
571+
if err := e.EncodeToken(xml.CharData(dc.Classification)); err != nil {
572+
return err
573+
}
574+
}
575+
if err := e.EncodeToken(xml.EndElement{Name: classStart.Name}); err != nil {
576+
return err
577+
}
578+
}
579+
580+
// Encode governance
581+
if dc.Governance != nil {
582+
govStart := xml.StartElement{Name: xml.Name{Local: "governance"}}
583+
if err := e.EncodeElement(dc.Governance, govStart); err != nil {
584+
return err
585+
}
586+
}
587+
588+
// Encode source URLs
589+
if dc.Source != nil && len(*dc.Source) > 0 {
590+
sourceStart := xml.StartElement{Name: xml.Name{Local: "source"}}
591+
if err := e.EncodeToken(sourceStart); err != nil {
592+
return err
593+
}
594+
for _, url := range *dc.Source {
595+
urlStart := xml.StartElement{Name: xml.Name{Local: "url"}}
596+
if err := e.EncodeElement(url, urlStart); err != nil {
597+
return err
598+
}
599+
}
600+
if err := e.EncodeToken(xml.EndElement{Name: sourceStart.Name}); err != nil {
601+
return err
602+
}
603+
}
604+
605+
// Encode destination URLs
606+
if dc.Destination != nil && len(*dc.Destination) > 0 {
607+
destStart := xml.StartElement{Name: xml.Name{Local: "destination"}}
608+
if err := e.EncodeToken(destStart); err != nil {
609+
return err
610+
}
611+
for _, url := range *dc.Destination {
612+
urlStart := xml.StartElement{Name: xml.Name{Local: "url"}}
613+
if err := e.EncodeElement(url, urlStart); err != nil {
614+
return err
615+
}
616+
}
617+
if err := e.EncodeToken(xml.EndElement{Name: destStart.Name}); err != nil {
618+
return err
619+
}
620+
}
621+
622+
return e.EncodeToken(xml.EndElement{Name: start.Name})
623+
}
624+
625+
// UnmarshalXML implements custom XML unmarshaling for DataClassification to support the v1.6 dataflow format
626+
func (dc *DataClassification) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
627+
// Parse name and description from attributes
628+
for _, attr := range start.Attr {
629+
switch attr.Name.Local {
630+
case "name":
631+
dc.Name = attr.Value
632+
case "description":
633+
dc.Description = attr.Value
634+
}
635+
}
636+
637+
// Parse child elements
638+
for {
639+
token, err := d.Token()
640+
if err != nil {
641+
return err
642+
}
643+
644+
switch el := token.(type) {
645+
case xml.StartElement:
646+
switch el.Name.Local {
647+
case "classification":
648+
// Parse flow attribute
649+
for _, attr := range el.Attr {
650+
if attr.Name.Local == "flow" {
651+
dc.Flow = DataFlow(attr.Value)
652+
}
653+
}
654+
// Parse classification text
655+
var content string
656+
if err := d.DecodeElement(&content, &el); err != nil {
657+
return err
658+
}
659+
dc.Classification = content
660+
661+
case "governance":
662+
var gov DataGovernance
663+
if err := d.DecodeElement(&gov, &el); err != nil {
664+
return err
665+
}
666+
dc.Governance = &gov
667+
668+
case "source":
669+
var urls []string
670+
for {
671+
token, err := d.Token()
672+
if err != nil {
673+
return err
674+
}
675+
if end, ok := token.(xml.EndElement); ok && end.Name.Local == "source" {
676+
break
677+
}
678+
if urlEl, ok := token.(xml.StartElement); ok && urlEl.Name.Local == "url" {
679+
var url string
680+
if err := d.DecodeElement(&url, &urlEl); err != nil {
681+
return err
682+
}
683+
urls = append(urls, url)
684+
}
685+
}
686+
if len(urls) > 0 {
687+
dc.Source = &urls
688+
}
689+
690+
case "destination":
691+
var urls []string
692+
for {
693+
token, err := d.Token()
694+
if err != nil {
695+
return err
696+
}
697+
if end, ok := token.(xml.EndElement); ok && end.Name.Local == "destination" {
698+
break
699+
}
700+
if urlEl, ok := token.(xml.StartElement); ok && urlEl.Name.Local == "url" {
701+
var url string
702+
if err := d.DecodeElement(&url, &urlEl); err != nil {
703+
return err
704+
}
705+
urls = append(urls, url)
706+
}
707+
}
708+
if len(urls) > 0 {
709+
dc.Destination = &urls
710+
}
711+
}
712+
713+
case xml.EndElement:
714+
if el.Name.Local == "dataflow" {
715+
return nil
716+
}
717+
}
718+
}
719+
}
720+
545721
var xmlNamespaces = map[SpecVersion]string{
546722
SpecVersion1_0: "http://cyclonedx.org/schema/bom/1.0",
547723
SpecVersion1_1: "http://cyclonedx.org/schema/bom/1.1",
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.6",
4+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
5+
"version": 1,
6+
"services": [
7+
{
8+
"bom-ref": "api-service",
9+
"name": "Payment API",
10+
"version": "1.0.0",
11+
"description": "Payment processing service",
12+
"data": [
13+
{
14+
"flow": "inbound",
15+
"classification": "PII",
16+
"name": "Customer Payment Data",
17+
"description": "Credit card and billing information from customers",
18+
"governance": {
19+
"custodians": [
20+
{
21+
"organization": {
22+
"name": "Security Team",
23+
"url": [
24+
"https://security.example.com"
25+
]
26+
}
27+
}
28+
],
29+
"stewards": [
30+
{
31+
"organization": {
32+
"name": "Data Governance"
33+
}
34+
}
35+
]
36+
},
37+
"source": [
38+
"https://frontend.example.com",
39+
"urn:cdx:f08a6ccd-4dce-4759-bd84-c626675d60a7/1#web-app"
40+
],
41+
"destination": [
42+
"https://payment-processor.example.com"
43+
]
44+
},
45+
{
46+
"flow": "outbound",
47+
"classification": "public",
48+
"name": "Transaction Receipt",
49+
"description": "Receipt data sent to customers",
50+
"source": [
51+
"https://payment-processor.example.com"
52+
],
53+
"destination": [
54+
"https://email-service.example.com",
55+
"https://frontend.example.com"
56+
]
57+
},
58+
{
59+
"flow": "bi-directional",
60+
"classification": "confidential"
61+
}
62+
]
63+
}
64+
]
65+
}
66+

testdata/snapshots/cyclonedx-go-TestRoundTripXML-func1-valid-annotation.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@
7676
<authenticated>true</authenticated>
7777
<x-trust-boundary>true</x-trust-boundary>
7878
<data>
79-
<classification flow="bi-directional">pubic</classification>
79+
<dataflow>
80+
<classification flow="bi-directional">pubic</classification>
81+
</dataflow>
8082
</data>
8183
</service>
8284
</annotator>

testdata/snapshots/cyclonedx-go-TestRoundTripXML-func1-valid-release-notes.xml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,18 @@
7777
<authenticated>true</authenticated>
7878
<x-trust-boundary>true</x-trust-boundary>
7979
<data>
80-
<classification flow="inbound">PII</classification>
81-
<classification flow="outbound">PIFI</classification>
82-
<classification flow="bi-directional">pubic</classification>
83-
<classification flow="unknown">partner-data</classification>
80+
<dataflow>
81+
<classification flow="inbound">PII</classification>
82+
</dataflow>
83+
<dataflow>
84+
<classification flow="outbound">PIFI</classification>
85+
</dataflow>
86+
<dataflow>
87+
<classification flow="bi-directional">pubic</classification>
88+
</dataflow>
89+
<dataflow>
90+
<classification flow="unknown">partner-data</classification>
91+
</dataflow>
8492
</data>
8593
<licenses>
8694
<license>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.6" serialNumber="urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79" version="1">
3+
<services>
4+
<service bom-ref="api-service">
5+
<name>Payment API</name>
6+
<version>1.0.0</version>
7+
<description>Payment processing service</description>
8+
<data>
9+
<dataflow name="Customer Payment Data" description="Credit card and billing information from customers">
10+
<classification flow="inbound">PII</classification>
11+
<governance>
12+
<custodians>
13+
<custodian>
14+
<organization>
15+
<name>Security Team</name>
16+
<url>https://security.example.com</url>
17+
</organization>
18+
</custodian>
19+
</custodians>
20+
<stewards>
21+
<steward>
22+
<organization>
23+
<name>Data Governance</name>
24+
</organization>
25+
</steward>
26+
</stewards>
27+
</governance>
28+
<source>
29+
<url>https://frontend.example.com</url>
30+
<url>urn:cdx:f08a6ccd-4dce-4759-bd84-c626675d60a7/1#web-app</url>
31+
</source>
32+
<destination>
33+
<url>https://payment-processor.example.com</url>
34+
</destination>
35+
</dataflow>
36+
<dataflow name="Transaction Receipt" description="Receipt data sent to customers">
37+
<classification flow="outbound">public</classification>
38+
<source>
39+
<url>https://payment-processor.example.com</url>
40+
</source>
41+
<destination>
42+
<url>https://email-service.example.com</url>
43+
<url>https://frontend.example.com</url>
44+
</destination>
45+
</dataflow>
46+
<dataflow>
47+
<classification flow="bi-directional">confidential</classification>
48+
</dataflow>
49+
</data>
50+
</service>
51+
</services>
52+
</bom>

0 commit comments

Comments
 (0)