diff --git a/cmd/cocli b/cmd/cocli new file mode 100644 index 0000000..1571346 Binary files /dev/null and b/cmd/cocli differ diff --git a/cmd/coswid.go b/cmd/coswid.go new file mode 100644 index 0000000..0aeffab --- /dev/null +++ b/cmd/coswid.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "fmt" +) + +var coswidCmd = &cobra.Command{ + Use: "coswid", + Short: "A brief description of your command", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("coswid command executed") + return nil + }, +} + +func init() { + rootCmd.AddCommand(coswidCmd) +} \ No newline at end of file diff --git a/cmd/coswidCreate.go b/cmd/coswidCreate.go new file mode 100644 index 0000000..cc02c8c --- /dev/null +++ b/cmd/coswidCreate.go @@ -0,0 +1,146 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "path/filepath" + + "github.com/xeipuuv/gojsonschema" + "github.com/fxamacker/cbor/v2" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/veraison/swid" +) + +type CustomSoftwareIdentity struct { + swid.SoftwareIdentity + Evidence struct { + Type string `json:"type"` + Value string `json:"value"` + } `json:"evidence"` +} + +var ( + coswidCreateTemplate string + coswidCreateOutputDir string +) + +var coswidCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a CBOR-encoded CoSWID from the supplied JSON template", + Long: `Create a CBOR-encoded CoSWID from the supplied JSON template. + +Create a CoSWID from template t1.json and save it to the current directory. + + cocli coswid create --template=t1.json + +Create a CoSWID from template t1.json and save it to the specified directory. +`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkCoswidCreateArgs(); err != nil { + return err + } + + // Validate JSON against schema before processing + schemaPath := "D:/opensource/cocli/data/coswid/templates/coswid-schema.json" + err := validateJSON(coswidCreateTemplate, schemaPath) + if err != nil { + return fmt.Errorf("JSON validation failed: %v", err) + } + + cborFile, err := coswidTemplateToCBOR(coswidCreateTemplate, coswidCreateOutputDir) + if err != nil { + return fmt.Errorf("error creating CBOR: %v", err) + } + fmt.Printf(">> created %q from %q\n", cborFile, coswidCreateTemplate) + + return nil + }, +} + +func checkCoswidCreateArgs() error { + if coswidCreateTemplate == "" { + return fmt.Errorf("template file is required") + } + return nil +} + +func coswidTemplateToCBOR(tmplFile, outputDir string) (string, error) { + var ( + tmplData []byte + coswidCBOR []byte + s CustomSoftwareIdentity + coswidFile string + err error + ) + + // Read the template file + tmplData, err = afero.ReadFile(afero.NewOsFs(), tmplFile) + if err != nil { + return "", fmt.Errorf("unable to read template file: %v", err) + } + + // Parse the JSON into the custom struct + err = json.Unmarshal(tmplData, &s) + if err != nil { + return "", fmt.Errorf("error decoding template from %s: %v", tmplFile, err) + } + + // Debugging: Print the parsed CustomSoftwareIdentity object + fmt.Println("Decoded CustomSoftwareIdentity object:") + fmt.Printf("%+v\n", s) + + // Encode the struct to CBOR using fxamacker/cbor + coswidCBOR, err = cbor.Marshal(s) + if err != nil { + return "", fmt.Errorf("error encoding to CBOR: %v", err) + } + + // Generate the output file name + coswidFile = makeFileName(outputDir, tmplFile, ".cbor") + + // Write the CBOR data to the output file + err = afero.WriteFile(afero.NewOsFs(), coswidFile, coswidCBOR, 0644) + if err != nil { + return "", fmt.Errorf("error writing CBOR file: %v", err) + } + + return coswidFile, nil +} + + +// validateJSON validates the JSON template against the provided schema +func validateJSON(tmplFile, schemaFile string) error { + schemaLoader := gojsonschema.NewReferenceLoader("file://" + filepath.ToSlash(schemaFile)) + documentLoader := gojsonschema.NewReferenceLoader("file://" + filepath.ToSlash(tmplFile)) + + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return fmt.Errorf("error during JSON validation: %v", err) + } + + if !result.Valid() { + for _, desc := range result.Errors() { + fmt.Printf("- %s\n", desc) + } + return fmt.Errorf("schema validation failed: JSON does not conform to schema") + } + + fmt.Println("JSON validation successful.") + return nil +} + +func init() { + coswidCmd.AddCommand(coswidCreateCmd) + coswidCreateCmd.Flags().StringVarP(&coswidCreateTemplate, "template", "t", "", "a CoSWID template file (in JSON format)") + coswidCreateCmd.Flags().StringVarP(&coswidCreateOutputDir, "output-dir", "o", ".", "output directory for CBOR file") + + // Handle required flag errors + if err := coswidCreateCmd.MarkFlagRequired("template"); err != nil { + // Since we're in init(), we can only panic on critical errors + panic(fmt.Sprintf("Failed to mark 'template' flag as required: %v", err)) + } + if err := coswidCreateCmd.MarkFlagRequired("output-dir"); err != nil { + panic(fmt.Sprintf("Failed to mark 'output-dir' flag as required: %v", err)) + } +} \ No newline at end of file diff --git a/cmd/coswidCreate_test.go b/cmd/coswidCreate_test.go new file mode 100644 index 0000000..dfd3329 --- /dev/null +++ b/cmd/coswidCreate_test.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "testing" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" +) + +func TestCheckCoswidCreateArgs(t *testing.T) { + // Setup + coswidCreateTemplate = "" + + // Test cases + err := checkCoswidCreateArgs() + assert.Error(t, err, "template file is required") + + coswidCreateTemplate = "template.json" + err = checkCoswidCreateArgs() + assert.NoError(t, err) +} + +func TestCoswidTemplateToCBOR(t *testing.T) { + // Setup + template := `{"evidence": {"type": "test", "value": "test"}}` + afero.WriteFile(afero.NewOsFs(), "template.json", []byte(template), 0644) + + // Test case + output, err := coswidTemplateToCBOR("template.json", ".") + assert.NoError(t, err) + assert.NotEmpty(t, output) +} + +func TestValidateJSON(t *testing.T) { + // Setup + template := `{"evidence": {"type": "test", "value": "test"}}` + schema := `{"type": "object"}` + afero.WriteFile(afero.NewOsFs(), "template.json", []byte(template), 0644) + afero.WriteFile(afero.NewOsFs(), "schema.json", []byte(schema), 0644) + + // Test case + err := validateJSON("template.json", "schema.json") + assert.NoError(t, err) +} diff --git a/cmd/coswidDisplay.go b/cmd/coswidDisplay.go new file mode 100644 index 0000000..d88fbde --- /dev/null +++ b/cmd/coswidDisplay.go @@ -0,0 +1,134 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "encoding/json" + + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/veraison/swid" +) + +var ( + coswidDisplayFile string + coswidDisplayDir string +) + +var coswidDisplayCmd = &cobra.Command{ + Use: "display", + Short: "Display one or more CBOR-encoded CoSWID(s) in human-readable (JSON) format", + Long: `Display one or more CBOR-encoded CoSWID(s) in human-readable (JSON) format. +You can supply individual CoSWID files or directories containing CoSWID files. + +Display CoSWID in file s.cbor. + + cocli coswid display --file=s.cbor + +Display CoSWIDs in files s1.cbor, s2.cbor and any cbor file in the coswids/ directory. + + cocli coswid display --file=s1.cbor --file=s2.cbor --dir=coswids +`, + RunE: func(cmd *cobra.Command, args []string) error { + // Validate input arguments + if err := checkCoswidDisplayArgs(); err != nil { + return err + } + + filesList := gatherFiles([]string{coswidDisplayFile}, []string{coswidDisplayDir}, ".cbor") + if len(filesList) == 0 { + return fmt.Errorf("no CoSWID files found") + } + + for _, file := range filesList { + if err := displayCoswid(file); err != nil { + fmt.Printf("Error displaying %s: %v\n", file, err) + } + } + + return nil + }, +} + +func checkCoswidDisplayArgs() error { + if coswidDisplayFile == "" && coswidDisplayDir == "" { + return fmt.Errorf("no CoSWID file or directory supplied") + } + return nil +} + +func gatherFiles(files []string, dirs []string, ext string) []string { + collectedMap := make(map[string]struct{}) + var collected []string + var walkErr error + + // Collect files from specified file paths + for _, file := range files { + if filepath.Ext(file) == ext { + collectedMap[file] = struct{}{} + } + } + + // Collect files from specified directories + for _, dir := range dirs { + if dir != "" { + exists, err := afero.Exists(fs, dir) + if err == nil && exists { + walkErr = afero.Walk(fs, dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("error accessing path %s: %v", path, err) + } + if !info.IsDir() && filepath.Ext(path) == ext { + collectedMap[path] = struct{}{} + } + return nil + }) + if walkErr != nil { + fmt.Printf("Warning: error walking directory %s: %v\n", dir, walkErr) + } + } + } + } + + // Convert map keys to slice + for file := range collectedMap { + collected = append(collected, file) + } + + return collected +} + +func displayCoswid(file string) error { + fmt.Printf("Processing file: %s\n", file) + var ( + coswidCBOR []byte + s swid.SoftwareIdentity + err error + ) + + // Read the CBOR file + if coswidCBOR, err = afero.ReadFile(fs, file); err != nil { + return fmt.Errorf("error reading file %s: %w", file, err) + } + + // Decode CBOR to SoftwareIdentity + if err = s.FromCBOR(coswidCBOR); err != nil { + return fmt.Errorf("error decoding CoSWID from %s: %w", file, err) + } + + // Convert to JSON + coswidJSON, err := json.MarshalIndent(&s, "", " ") + if err != nil { + return fmt.Errorf("error marshaling CoSWID to JSON: %w", err) + } + + fmt.Printf(">> [%s]\n%s\n", file, string(coswidJSON)) + return nil +} + +func init() { + coswidCmd.AddCommand(coswidDisplayCmd) + coswidDisplayCmd.Flags().StringVarP(&coswidDisplayFile, "file", "f", "", "a CoSWID file (in CBOR format)") + coswidDisplayCmd.Flags().StringVarP(&coswidDisplayDir, "dir", "d", "", "a directory containing CoSWID files") +} \ No newline at end of file diff --git a/cmd/coswidDisplay_test.go b/cmd/coswidDisplay_test.go new file mode 100644 index 0000000..90faa7c --- /dev/null +++ b/cmd/coswidDisplay_test.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "testing" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" +) + +func TestCheckCoswidDisplayArgs(t *testing.T) { + // Setup + coswidDisplayFile = "" + coswidDisplayDir = "" + + // Test cases + err := checkCoswidDisplayArgs() + assert.Error(t, err, "no CoSWID file or directory supplied") + + coswidDisplayFile = "test.cbor" + err = checkCoswidDisplayArgs() + assert.NoError(t, err) +} + +func TestGatherFiles(t *testing.T) { + // Setup + fs = afero.NewMemMapFs() + afero.WriteFile(fs, "test.cbor", []byte{}, 0644) + afero.WriteFile(fs, "dir/test.cbor", []byte{}, 0644) + + // Test case + files := gatherFiles([]string{"test.cbor"}, []string{"dir"}, ".cbor") + assert.Len(t, files, 2) +} + +func TestDisplayCoswid(t *testing.T) { + // Setup + fs = afero.NewMemMapFs() + afero.WriteFile(fs, "test.cbor", []byte{0xA1, 0x01, 0x02}, 0644) + + // Test case + err := displayCoswid("test.cbor") + assert.NoError(t, err) +} diff --git a/cmd/coswidValidate.go b/cmd/coswidValidate.go new file mode 100644 index 0000000..0bd5de9 --- /dev/null +++ b/cmd/coswidValidate.go @@ -0,0 +1,208 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "encoding/base64" + + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/xeipuuv/gojsonschema" + "github.com/fxamacker/cbor/v2" // Added CBOR package +) + +var ( + coswidValidateFile string + coswidValidateSchema string +) + +var coswidKeyMap = map[uint64]string{ + 0: "schema-version", + 1: "tag-id", + 2: "software-name", + 3: "tag-version", + 4: "patch-level", + 5: "version", + 6: "version-scheme", + 7: "lang", + 8: "directory", + 9: "file", + 10: "process", + 11: "resource", + 12: "size", + 13: "file-version", + 14: "entity", + 15: "evidence", + 16: "link", + 17: "payload", + 18: "hash", + 19: "hash-alg-id", + 20: "hash-value", +} + +var coswidValidateCmd = &cobra.Command{ + Use: "validate", + Short: "Validate a CBOR-encoded CoSWID against the provided JSON schema", + Long: `Validate a CBOR-encoded CoSWID against the provided JSON schema + + Validate the CoSWID in file s.cbor against the schema schema.json. + + cocli coswid validate --file=s.cbor --schema=schema.json + `, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkCoswidValidateArgs(); err != nil { + return err + } + + if err := validateCoswid(coswidValidateFile, coswidValidateSchema); err != nil { + return err + } + + fmt.Printf(">> validated %q against %q\n", coswidValidateFile, coswidValidateSchema) + return nil + }, +} + +func checkCoswidValidateArgs() error { + if coswidValidateFile == "" { + return fmt.Errorf("no CoSWID file supplied") + } + if coswidValidateSchema == "" { + return fmt.Errorf("no schema supplied") + } + return nil +} + +func validateCoswid(file, schema string) error { + var ( + coswidCBOR []byte + coswidJSON []byte + err error + ) + + if coswidCBOR, err = afero.ReadFile(fs, file); err != nil { + return fmt.Errorf("error loading CoSWID from %s: %w", file, err) + } + + // Decode CBOR with numeric key handling + var data map[interface{}]interface{} + if err = cbor.Unmarshal(coswidCBOR, &data); err != nil { + return fmt.Errorf("error decoding CBOR from %s: %w", file, err) + } + + // Convert map[interface{}]interface{} to map[string]interface{} + stringMap := make(map[string]interface{}) + for key, value := range data { + strKey := convertKeyToString(key) + convertedValue := convertValue(value) + stringMap[strKey] = convertedValue + } + + // Debug: Iterate and print types + for key, value := range stringMap { + fmt.Printf("Field: %s, Type: %T, Value: %v\n", key, value, value) + + switch key { + case "tag-id", "device-id", "location": + if str, ok := value.(string); !ok { + return fmt.Errorf("field %s is expected to be string, but got %T", key, value) + } else { + _ = str + } + + case "software-name": + // Accept either string or map + switch v := value.(type) { + case string: + // OK + case map[string]interface{}: + // Handle it as a nested object if needed + _ = v + default: + return fmt.Errorf("field %s has unexpected type %T", key, value) + } + + case "tag-version", "hash-alg-id": + switch v := value.(type) { + case int, int32, int64: + case float64: + intValue := int(v) + stringMap[key] = intValue + default: + return fmt.Errorf("field %s is expected to be integer, but got %T", key, value) + } + + default: + // Other fields + } + } + + // Marshal the decoded data to JSON + if coswidJSON, err = json.Marshal(stringMap); err != nil { + return fmt.Errorf("error marshaling CoSWID to JSON: %w", err) + } + + schemaLoader := gojsonschema.NewReferenceLoader("file:///" + schema) + documentLoader := gojsonschema.NewBytesLoader(coswidJSON) + + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return fmt.Errorf("error validating CoSWID from %s: %w", file, err) + } + + if !result.Valid() { + return fmt.Errorf("CoSWID from %s is invalid: %v", file, result.Errors()) + } + + return nil +} + +func convertKeyToString(key interface{}) string { + switch k := key.(type) { + case string: + return k + case int: + if mappedKey, ok := coswidKeyMap[uint64(k)]; ok { + return mappedKey + } + return fmt.Sprintf("%d", k) + case uint64: + if mappedKey, ok := coswidKeyMap[k]; ok { + return mappedKey + } + return fmt.Sprintf("%d", k) + default: + return fmt.Sprintf("%v", k) + } +} + +func convertValue(value interface{}) interface{} { + switch v := value.(type) { + case map[interface{}]interface{}: + // Convert nested maps + m := make(map[string]interface{}) + for k, val := range v { + strKey := convertKeyToString(k) + m[strKey] = convertValue(val) + } + return m + case []interface{}: + // Convert slice elements + slice := make([]interface{}, len(v)) + for i, val := range v { + slice[i] = convertValue(val) + } + return slice + case []uint8: + // Convert byte arrays to base64 + return base64.StdEncoding.EncodeToString(v) + default: + return v + } +} + +func init() { + coswidCmd.AddCommand(coswidValidateCmd) + coswidValidateCmd.Flags().StringVarP(&coswidValidateFile, "file", "f", "", "a CoSWID file (in CBOR format)") + coswidValidateCmd.Flags().StringVarP(&coswidValidateSchema, "schema", "s", "", "a JSON schema file") +} diff --git a/cmd/coswidValidate_test.go b/cmd/coswidValidate_test.go new file mode 100644 index 0000000..5713cb5 --- /dev/null +++ b/cmd/coswidValidate_test.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "testing" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestCheckCoswidValidateArgs(t *testing.T) { + // Setup + coswidValidateFile = "" + coswidValidateSchema = "" + + // Test cases + err := checkCoswidValidateArgs() + assert.Error(t, err, "no CoSWID file supplied") + + coswidValidateFile = "test.cbor" + err = checkCoswidValidateArgs() + assert.Error(t, err, "no schema supplied") + + coswidValidateSchema = "schema.json" + err = checkCoswidValidateArgs() + assert.NoError(t, err) +} + +func TestValidateCoswid(t *testing.T) { + // Setup + fs = afero.NewMemMapFs() + afero.WriteFile(fs, "test.cbor", []byte{0xA1, 0x01, 0x02}, 0644) + afero.WriteFile(fs, "schema.json", []byte(`{"type": "object"}`), 0644) + + // Test case + err := validateCoswid("test.cbor", "schema.json") + assert.NoError(t, err) +} + +func TestCoswidValidateCmd(t *testing.T) { + // Setup + cmd := &cobra.Command{} + + // Test case + coswidValidateCmd.RunE(cmd, []string{}) + assert.NoError(t, cmd.Execute()) +} diff --git a/cmd/coswid_test.go b/cmd/coswid_test.go new file mode 100644 index 0000000..d7e9870 --- /dev/null +++ b/cmd/coswid_test.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "testing" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestCoswidCmd(t *testing.T) { + // Setup + cmd := &cobra.Command{} + + // Test case + err := coswidCmd.RunE(cmd, []string{}) + assert.NoError(t, err) + assert.NoError(t, cmd.Execute()) +} diff --git a/cmd/output.cbor b/cmd/output.cbor new file mode 100644 index 0000000..1bff9ec Binary files /dev/null and b/cmd/output.cbor differ diff --git a/cocli.exe b/cocli.exe new file mode 100644 index 0000000..e4824cd Binary files /dev/null and b/cocli.exe differ diff --git a/data/coswid/coswid-example.cbor b/data/coswid/coswid-example.cbor new file mode 100644 index 0000000..18b2abf Binary files /dev/null and b/data/coswid/coswid-example.cbor differ diff --git a/data/coswid/coswid-full.cbor b/data/coswid/coswid-full.cbor new file mode 100644 index 0000000..3fcdce7 Binary files /dev/null and b/data/coswid/coswid-full.cbor differ diff --git a/data/coswid/coswid-meta-full.cbor b/data/coswid/coswid-meta-full.cbor new file mode 100644 index 0000000..7b42dc9 Binary files /dev/null and b/data/coswid/coswid-meta-full.cbor differ diff --git a/data/coswid/coswid-meta-mini.cbor b/data/coswid/coswid-meta-mini.cbor new file mode 100644 index 0000000..7b42dc9 Binary files /dev/null and b/data/coswid/coswid-meta-mini.cbor differ diff --git a/data/coswid/readme.md b/data/coswid/readme.md new file mode 100644 index 0000000..a8e86dc --- /dev/null +++ b/data/coswid/readme.md @@ -0,0 +1,210 @@ +# CoSWID Template Format + +## Introduction +**CoSWID stands for Concise Software Identification Tag, a structured format for representing software identification information in a compact manner.** +**CoSWID is designed to facilitate software identification in various contexts, including software inventory management, compliance verification, and security assessments.** + +## Conceptual Overview +### What Is CoSWID? +**CoSWID is a data model that captures essential attributes of software identification, which might include:** + +- **Tag ID:** A unique identifier for the software. +- **Tag Version:** The version of the CoSWID. +- **Software Name:** The name of the software being identified. +- **Profile:** A URI that identifies the specification or profile associated with the CoSWID. +- **Validity:** The time period during which the CoSWID is considered valid. +- **Entity:** Metadata describing organizations or roles involved in the software's lifecycle. +- **Links:** References to related resources or manifests. + +### CoSWID in Software Management +**In software management contexts, CoSWID provides a standardized way to represent software components, allowing for:** + +- **Efficient Identification:** Compact representation of software metadata. +- **Interoperability:** Compatibility with existing software identification standards. +- **Enhanced Security:** Facilitating trust and verification processes in software supply chains. + +## Template Structure +**A CoSWID template is typically represented in JSON for human-friendly editing.** +**At a minimum, it includes tag-id (a unique identifier).** +**Optional fields like tag-version, software-name, profile, validity, entity, and link provide deeper context:** + +```json +{ + "tag-id": "", + "tag-version": , + "software-name": "", + "profile": "", + "validity": { + "not-before": "", + "not-after": "" + }, + "entity": [ ... ], + "link": [ ... ] +} +``` + +## Top-Level Fields +- **tag-id (String/UUID):** A globally unique identifier for the CoSWID. +- **tag-version (Integer):** The version of the CoSWID. +- **software-name (String):** The name of the software being identified. +- **profile (String, optional):** A URI referencing a particular standard or specification. +- **validity (Object, optional):** A time window (not-before / not-after) during which the CoSWID is valid. +- **entity (Array, optional):** An array of organizations or roles involved in the software's lifecycle. +- **link (Array, optional):** References to related resources or manifests. + +## Key Components +### Tag ID +- **Type:** String (UUID) +- **Description:** A unique identifier for the CoSWID. +- **Example:** "123e4567-e89b-12d3-a456-426614174000" + +### Tag Version +- **Type:** Integer +- **Description:** The version of the CoSWID. +- **Example:** 0 + +### Software Name +- **Type:** String +- **Description:** The name of the software being identified. +- **Example:** "Example Software" + +### Profile +- **Type:** String (URI) +- **Description:** A URI that identifies the specification or profile associated with the CoSWID. +- **Example:** "http://example.com/cosmid/profile/1" + +### Validity +- **Type:** Object +- **Fields:** + - **not-before:** The earliest valid timestamp for using this CoSWID. + - **not-after:** The expiry timestamp after which the CoSWID is invalid. + +### Entity +- **Type:** Array of Objects +- **Purpose:** Identifies the organizations or individuals related to the software. +- **Fields:** + - **entity-name:** Human-readable name of the entity. + - **reg-id:** A registration/domain identifier (e.g., https://example.com). + - **role:** Array of roles (e.g., [ "tag-creator" ]). + +### Link +- **Type:** Array of Objects +- **Purpose:** References to related resources or manifests. +- **Fields:** + - **href:** A URI pointing to the related resource. + - **thumbprint:** An object containing hash information for integrity verification. + - **hash-alg-id:** The identifier for the hash algorithm used. + - **hash-value:** The computed hash value. + +## Additional Files Overview +### coswid-example.json +- **Purpose:** Provides a basic example of a CoSWID structure. +- **Key Fields:** + - **tag-id:** "123e4567-e89b-12d3-a456-426614174000" + - **software-name:** "Example Software" + - **validity:** Specifies the valid time frame. + +### coswid-full.json +- **Purpose:** Contains a comprehensive CoSWID structure with additional fields. +- **Key Fields:** + - **Includes all fields from coswid-example.json plus:** + - **link:** References to related resources with thumbprint for integrity. + +### coswid-meta-full.json +- **Purpose:** Contains metadata related to the signing of the CoSWID. +- **Key Fields:** + - **signer:** Information about the entity that signed the CoSWID. + - **validity:** Similar to the main CoSWID, indicating the validity period. + +### coswid-meta-mini.json +- **Purpose:** A simplified version of the metadata file. +- **Key Fields:** + - **signer:** Basic information about the signing entity without detailed validity. + +### coswid-schema.json +- **Purpose:** Defines the JSON schema for validating CoSWID files. +- **Key Features:** + - **Specifies the structure and types of each field in the CoSWID.** + - **Ensures compliance with the CoSWID format.** + +## Detailed Field Descriptions +### Validity Object +- **Fields:** + - **not-before:** The date and time when the CoSWID becomes valid. + - **not-after:** The date and time when the CoSWID expires. + +### Entity Array +- **Example:** +```json +"entity": [ + { + "entity-name": "Example Ltd.", + "reg-id": "https://example.com", + "role": ["tag-creator"] + } +] +``` + +### Link Array +- **Example:** +```json +"link": [ + { + "href": "https://parent.example/rims/ccb3aa85-61b4-40f1-848e-02ad6e8a254b", + "thumbprint": { + "hash-alg-id": 1, + "hash-value": "5Fty9cDAtXLbTY06t+l/No/3TmI0eoJN7LZ6hOUiTXU=" + } + } +] +``` + +## Use Cases +### Software Inventory Management +- **Description:** CoSWID can be used to maintain an accurate inventory of software assets within an organization. +- **Benefits:** + - **Simplifies tracking of software versions and compliance.** + - **Enhances visibility into software usage and licensing.** + +### Compliance Verification +- **Description:** Organizations can use CoSWID to demonstrate compliance with software licensing and regulatory requirements. +- **Benefits:** + - **Provides a clear audit trail of software identification.** + - **Facilitates easier reporting and verification processes.** + +### Security Assessments +- **Description:** CoSWID aids in assessing the security posture of software components. +- **Benefits:** + - **Enables identification of vulnerabilities in software dependencies.** + - **Supports risk management and mitigation strategies.** + +## Best Practices +### Consistent Use of UUIDs +- **Always use UUIDs for tag-id to ensure uniqueness across different systems.** + +### Regular Updates +- **Keep the CoSWID files updated with the latest software versions and metadata.** + +### Validation Against Schema +- **Validate CoSWID files against the provided JSON schema to ensure compliance with the expected format.** + +## Conclusion +**The CoSWID template format provides a robust framework for software identification, ensuring that essential metadata is captured in a standardized manner.** +**By adhering to this structure, organizations can enhance their software management practices, improve compliance, and facilitate better security measures.** + +**For further details, please refer to the official CoSWID specification or the relevant documentation associated with this repository.** + +## References +- [**CoSWID Specification**](https://datatracker.ietf.org/doc/rfc9393/) + +## Visual Overview +```mermaid +graph TD; + A[CoSWID Template] --> B[Tag ID]; + A --> C[Tag Version]; + A --> D[Software Name]; + A --> E[Profile]; + A --> F[Validity]; + A --> G[Entity]; + A --> H[Link]; +``` diff --git a/data/coswid/templates/coswid-example.json b/data/coswid/templates/coswid-example.json new file mode 100644 index 0000000..4bd4088 --- /dev/null +++ b/data/coswid/templates/coswid-example.json @@ -0,0 +1,19 @@ +{ + "tag-id": "123e4567-e89b-12d3-a456-426614174000", + "tag-version": 0, + "software-name": "Example Software", + "profile": "http://example.com/cosmid/profile/1", + "validity": { + "not-before": "2023-01-01T00:00:00Z", + "not-after": "2025-01-01T00:00:00Z" + }, + "entity": [ + { + "entity-name": "Example Ltd.", + "reg-id": "https://example.com", + "role": [ + "tag-creator" + ] + } + ] +} \ No newline at end of file diff --git a/data/coswid/templates/coswid-full.json b/data/coswid/templates/coswid-full.json new file mode 100644 index 0000000..a12293e --- /dev/null +++ b/data/coswid/templates/coswid-full.json @@ -0,0 +1,35 @@ +{ + "tag-id": "123e4567-e89b-12d3-a456-426614174000", + "tag-version": 1, + "software-name": "Example Software", + "profile": "http://example.com/cosmid/profile/1", + "validity": { + "not-before": "2023-01-01T00:00:00Z", + "not-after": "2025-01-01T00:00:00Z" + }, + "entity": [ + { + "entity-name": "Example Ltd signing key", + "reg-id": "https://example.com", + "role": ["tag-creator"] + } + ], + "version-scheme": "multipartnumeric", + "link": [ + { + "rel": "parent", + "href": "https://parent.example/rims/ccb3aa85-61b4-40f1-848e-02ad6e8a254b", + "thumbprint": { + "hash-alg-id": 1, + "hash-value": "5Fty9cDAtXLbTY06t+l/No/3TmI0eoJN7LZ6hOUiTXU=" + } + } + ], + "payload": { + "data": "Sample payload data" + }, + "evidence": { + "type": "example-evidence", + "value": "Evidence details" + } +} \ No newline at end of file diff --git a/data/coswid/templates/coswid-meta-full.json b/data/coswid/templates/coswid-meta-full.json new file mode 100644 index 0000000..158d43c --- /dev/null +++ b/data/coswid/templates/coswid-meta-full.json @@ -0,0 +1,17 @@ +{ + "tag-id": "123e4567-e89b-12d3-a456-426614174000", + "tag-version": 1, + "software-name": "Example Software", + "profile": "http://example.com/cosmid/profile/1", + "validity": { + "not-before": "2023-01-01T00:00:00Z", + "not-after": "2025-01-01T00:00:00Z" + }, + "entity": [ + { + "entity-name": "Example Ltd signing key", + "reg-id": "https://example.com", + "role": ["tag-creator"] + } + ] +} \ No newline at end of file diff --git a/data/coswid/templates/coswid-meta-mini.json b/data/coswid/templates/coswid-meta-mini.json new file mode 100644 index 0000000..0aabbd8 --- /dev/null +++ b/data/coswid/templates/coswid-meta-mini.json @@ -0,0 +1,13 @@ +{ + "tag-id": "123e4567-e89b-12d3-a456-426614174000", + "tag-version": 1, + "software-name": "Example Software", + "profile": "http://example.com/cosmid/profile/1", + "entity": [ + { + "entity-name": "Example Ltd signing key", + "reg-id": "https://example.com", + "role": ["tag-creator"] + } + ] +} diff --git a/data/coswid/templates/coswid-schema.json b/data/coswid/templates/coswid-schema.json new file mode 100644 index 0000000..dd71bbc --- /dev/null +++ b/data/coswid/templates/coswid-schema.json @@ -0,0 +1,498 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CoSMID Schema", + "type": "object", + "properties": { + "tag-id": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + }, + "tag-version": { + "type": "integer" + }, + "corpus": { + "type": "boolean" + }, + "patch": { + "type": "boolean" + }, + "supplemental": { + "type": "boolean" + }, + "software-name": { + "type": "string" + }, + "software-version": { + "type": "string" + }, + "version-scheme": { + "type": "string", + "enum": [ + "multipartnumeric", + "multipartnumeric-suffix", + "alphanumeric", + "decimal", + "semver" + ] + }, + "media": { + "type": "string" + }, + "software-meta": { + "type": "array", + "items": { + "type": "object", + "properties": { + "activation-status": { + "type": "string" + }, + "channel-type": { + "type": "string" + }, + "colloquial-version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "edition": { + "type": "string" + }, + "entitlement-data-required": { + "type": "boolean" + }, + "entitlement-key": { + "type": "string" + }, + "generator": { + "type": "string", + "pattern": "^[0-9a-fA-F]{32}$" + }, + "persistent-id": { + "type": "string" + }, + "product": { + "type": "string" + }, + "product-family": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "unspsc-code": { + "type": "string" + }, + "unspsc-version": { + "type": "string" + } + }, + "required": [ + "activation-status", + "channel-type", + "colloquial-version", + "description", + "edition", + "entitlement-data-required", + "entitlement-key", + "generator", + "persistent-id", + "product", + "product-family", + "revision", + "summary", + "unspsc-code", + "unspsc-version" + ] + } + }, + "entity": { + "type": "array", + "items": { + "type": "object", + "properties": { + "entity-name": { + "type": "string" + }, + "reg-id": { + "type": "string", + "format": "uri" + }, + "role": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "tag-creator", + "software-creator", + "aggregator", + "distributor", + "licensor", + "maintainer" + ] + } + }, + "thumbprint": { + "type": "object", + "properties": { + "hash-alg-id": { + "type": "integer" + }, + "hash-value": { + "type": "string" + } + }, + "required": [ + "hash-alg-id", + "hash-value" + ] + } + }, + "required": [ + "entity-name", + "role" + ] + } + }, + "link": { + "type": "array", + "items": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + }, + "href": { + "type": "string", + "format": "uri" + }, + "media": { + "type": "string" + }, + "ownership": { + "type": "string", + "enum": [ + "shared", + "private", + "abandon" + ] + }, + "rel": { + "type": "string", + "enum": [ + "ancestor", + "component", + "feature", + "installationmedia", + "packageinstaller", + "parent", + "patches", + "requires", + "see-also", + "supersedes", + "supplemental" + ] + }, + "media-type": { + "type": "string" + }, + "use": { + "type": "string", + "enum": [ + "optional", + "required", + "recommended" + ] + }, + "thumbprint": { + "type": "object", + "properties": { + "hash-alg-id": { + "type": "integer" + }, + "hash-value": { + "type": "string" + } + }, + "required": [ + "hash-alg-id", + "hash-value" + ] + } + }, + "required": [ + "href", + "rel" + ] + } + }, + "payload": { + "type": "object", + "properties": { + "resource-collection": { + "type": "object", + "properties": { + "directory": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "fs-name": { + "type": "string" + }, + "root": { + "type": "string" + }, + "path-elements": { + "type": "object", + "properties": { + "directory": { + "type": "array", + "items": { + "type": "object" + } + }, + "file": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + }, + "required": [ + "fs-name" + ] + } + }, + "file": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "fs-name": { + "type": "string" + }, + "root": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "file-version": { + "type": "string" + }, + "hash": { + "type": "object", + "properties": { + "hash-alg-id": { + "type": "integer" + }, + "hash-value": { + "type": "string" + } + }, + "required": [ + "hash-alg-id", + "hash-value" + ] + } + }, + "required": [ + "fs-name" + ] + } + }, + "process": { + "type": "array", + "items": { + "type": "object", + "properties": { + "process-name": { + "type": "string" + }, + "pid": { + "type": "integer" + } + }, + "required": [ + "process-name" + ] + } + }, + "resource": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + }, + "required": [ + "type" + ] + } + } + } + } + } + }, + "evidence": { + "type": "object", + "properties": { + "resource-collection": { + "type": "object", + "properties": { + "directory": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "fs-name": { + "type": "string" + }, + "root": { + "type": "string" + }, + "path-elements": { + "type": "object", + "properties": { + "directory": { + "type": "array", + "items": { + "type": "object" + } + }, + "file": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + }, + "required": [ + "fs-name" + ] + } + }, + "file": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "fs-name": { + "type": "string" + }, + "root": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "file-version": { + "type": "string" + }, + "hash": { + "type": "object", + "properties": { + "hash-alg-id": { + "type": "integer" + }, + "hash-value": { + "type": "string" + } + }, + "required": [ + "hash-alg-id", + "hash-value" + ] + } + }, + "required": [ + "fs-name" + ] + } + }, + "process": { + "type": "array", + "items": { + "type": "object", + "properties": { + "process-name": { + "type": "string" + }, + "pid": { + "type": "integer" + } + }, + "required": [ + "process-name" + ] + } + }, + "resource": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + }, + "required": [ + "type" + ] + } + } + } + }, + "date": { + "type": "integer" + }, + "device-id": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + }, + "required": [ + "tag-id", + "tag-version", + "software-name", + "entity" + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod index 1a993d3..ffda987 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,9 @@ require ( github.com/subosito/gotenv v1.2.0 // indirect github.com/veraison/eat v0.0.0-20210331113810-3da8a4dd42ff // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/crypto v0.26.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.11.0 // indirect diff --git a/go.sum b/go.sum index 95dd6cc..64e5570 100644 --- a/go.sum +++ b/go.sum @@ -331,6 +331,12 @@ github.com/veraison/swid v1.1.1-0.20230911094910-8ffdd07a22ca h1:osmCKwWO/xM68Kz github.com/veraison/swid v1.1.1-0.20230911094910-8ffdd07a22ca/go.mod h1:d5jt76uMNbTfQ+f2qU4Lt8RvWOTsv6PFgstIM1QdMH0= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=