From 80dd689da2964671973750c57b280755fb688492 Mon Sep 17 00:00:00 2001 From: Aditya Sirish Date: Wed, 25 Oct 2023 15:30:59 -0400 Subject: [PATCH] Make linter happy Signed-off-by: Aditya Sirish --- scai-gen/cmd/assert.go | 28 ++++++------ scai-gen/cmd/check.go | 80 +++++++++++++++++----------------- scai-gen/cmd/rd.go | 61 +++++++++++++------------- scai-gen/cmd/report.go | 26 +++++------ scai-gen/cmd/root.go | 6 +-- scai-gen/fileio/dsse.go | 14 +++--- scai-gen/fileio/map.go | 8 ++-- scai-gen/fileio/pb.go | 10 ++--- scai-gen/policy/attestation.go | 9 ++-- scai-gen/policy/checks.go | 10 ++--- scai-gen/policy/plaintext.go | 15 +++---- 11 files changed, 129 insertions(+), 138 deletions(-) diff --git a/scai-gen/cmd/assert.go b/scai-gen/cmd/assert.go index 4507db8..4a4afb6 100644 --- a/scai-gen/cmd/assert.go +++ b/scai-gen/cmd/assert.go @@ -2,10 +2,11 @@ package cmd import ( "fmt" + "github.com/in-toto/scai-demos/scai-gen/fileio" - ita "github.com/in-toto/attestation/go/v1" scai "github.com/in-toto/attestation/go/predicates/scai/v0" + ita "github.com/in-toto/attestation/go/v1" "github.com/spf13/cobra" "google.golang.org/protobuf/types/known/structpb" ) @@ -18,9 +19,9 @@ var assertCmd = &cobra.Command{ } var ( - targetFile string - conditionsFile string - evidenceFile string + targetFile string + conditionsFile string + evidenceFile string ) func init() { @@ -31,8 +32,8 @@ func init() { "", "Filename to write out the JSON-encoded object", ) - assertCmd.MarkFlagRequired("out-file") - + assertCmd.MarkFlagRequired("out-file") //nolint:errcheck + assertCmd.Flags().StringVarP( &targetFile, "target", @@ -58,8 +59,7 @@ func init() { ) } -func genAttrAssertion(cmd *cobra.Command, args []string) error { - +func genAttrAssertion(_ *cobra.Command, args []string) error { attribute := args[0] var target *ita.ResourceDescriptor @@ -88,18 +88,18 @@ func genAttrAssertion(cmd *cobra.Command, args []string) error { return err } } - + aa := &scai.AttributeAssertion{ - Attribute: attribute, - Target: target, + Attribute: attribute, + Target: target, Conditions: conditions, - Evidence: evidence, + Evidence: evidence, } err := aa.Validate() if err != nil { - return fmt.Errorf("Invalid SCAI attribute assertion: %w", err) + return fmt.Errorf("invalid SCAI attribute assertion: %w", err) } - + return fileio.WritePbToFile(aa, outFile) } diff --git a/scai-gen/cmd/check.go b/scai-gen/cmd/check.go index ba9c841..2d6b676 100644 --- a/scai-gen/cmd/check.go +++ b/scai-gen/cmd/check.go @@ -1,18 +1,19 @@ package cmd import ( - "path/filepath" - "fmt" "encoding/json" + "fmt" "io/fs" - "strings" "os" + "path/filepath" + "strings" + "github.com/in-toto/scai-demos/scai-gen/fileio" "github.com/in-toto/scai-demos/scai-gen/policy" "github.com/in-toto/attestation-verifier/verifier" - ita "github.com/in-toto/attestation/go/v1" scai "github.com/in-toto/attestation/go/predicates/scai/v0" + ita "github.com/in-toto/attestation/go/v1" "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/spf13/cobra" "google.golang.org/protobuf/encoding/protojson" @@ -58,10 +59,10 @@ func init() { "", "The filename of the YAML-encoded in-toto Layout", ) - layoutCmd.MarkFlagRequired("layout") + layoutCmd.MarkFlagRequired("layout") //nolint:errcheck } -func init() { +func init() { evCmd.Flags().StringVarP( &evidenceDir, "evidence-dir", @@ -69,7 +70,7 @@ func init() { "", "The directory containing evidence files", ) - evCmd.MarkFlagRequired("evidence-dir") + evCmd.MarkFlagRequired("evidence-dir") //nolint:errcheck evCmd.Flags().StringVarP( &policyFile, @@ -78,11 +79,10 @@ func init() { "", "The filename of the policy file", ) - evCmd.MarkFlagRequired("policy-file") + evCmd.MarkFlagRequired("policy-file") //nolint:errcheck } -func checkLayout(cmd *cobra.Command, args []string) error { - +func checkLayout(_ *cobra.Command, args []string) error { layout, err := verifier.LoadLayout(layoutFile) if err != nil { return err @@ -109,8 +109,7 @@ func checkLayout(cmd *cobra.Command, args []string) error { return verifier.Verify(layout, attestations, parameters) } -func checkEvidence(cmd *cobra.Command, args []string) error { - +func checkEvidence(_ *cobra.Command, args []string) error { attestationPath := args[0] fmt.Println("Reading attestation file", attestationPath) @@ -118,9 +117,9 @@ func checkEvidence(cmd *cobra.Command, args []string) error { if err != nil { return err } - + fmt.Println("Reading policy file", policyFile) - + policyBytes, err := os.ReadFile(policyFile) if err != nil { return err @@ -134,7 +133,7 @@ func checkEvidence(cmd *cobra.Command, args []string) error { fmt.Println("Checking attestation matches ID in policy") if !policy.MatchDigest(evPolicy.AttestationID, envBytes) { - return fmt.Errorf("Attestation does not match attestation ID in policy") + return fmt.Errorf("attestation does not match attestation ID in policy") } // now, let's get the Statement @@ -144,21 +143,21 @@ func checkEvidence(cmd *cobra.Command, args []string) error { if err := json.Unmarshal(envBytes, envelope); err != nil { return err } - + statement, err := getStatementDSSEPayload(envelope) if err != nil { return err } fmt.Println("Collecting all evidence files") - + evidenceFiles, err := getAllEvidenceFiles(evidenceDir) if err != nil { - return fmt.Errorf("Failed read evidence files in directory %s: %w", evidenceDir, err) + return fmt.Errorf("failed read evidence files in directory %s: %w", evidenceDir, err) } if statement.GetPredicateType() != "https://in-toto.io/attestation/scai/attribute-report/v0.2" { - return fmt.Errorf("Evidence checking only supported for SCAI attestations") + return fmt.Errorf("evidence checking only supported for SCAI attestations") } report, err := pbStructToSCAI(statement.GetPredicate()) @@ -168,7 +167,7 @@ func checkEvidence(cmd *cobra.Command, args []string) error { // validate the report if err := report.Validate(); err != nil { - return fmt.Errorf("Malformed SCAI Attribute Report: %w", err) + return fmt.Errorf("malformed SCAI Attribute Report: %w", err) } // order attribute assertions by evidence name @@ -181,28 +180,28 @@ func checkEvidence(cmd *cobra.Command, args []string) error { } fmt.Println("Checking policy rules...") - + for _, check := range evPolicy.Inspections { rules := check.ExpectedAttributes if len(rules) == 0 { - return fmt.Errorf("No rules for check %s", check.Name) + return fmt.Errorf("no rules for check %s", check.Name) } - + attrAssertion, ok := attrAssertions[check.Name] if !ok { - return fmt.Errorf("Attestation evidence missing %s", check.Name) + return fmt.Errorf("attestation evidence missing %s", check.Name) } fmt.Println("Validating attribute assertion format") if err := attrAssertion.Validate(); err != nil { - return fmt.Errorf("Malformed attribute assertion in attestation: %w", err) + return fmt.Errorf("malformed attribute assertion in attestation: %w", err) } ev := attrAssertion.GetEvidence() - + evContent, ok := evidenceFiles[ev.GetName()] if !ok { - return fmt.Errorf("Evidence file to check not found") + return fmt.Errorf("evidence file to check not found") } fmt.Println("Checking evidence content according to policy rules...") @@ -211,28 +210,27 @@ func checkEvidence(cmd *cobra.Command, args []string) error { case "text/plain": err := policy.ApplyPlaintextRules(string(evContent), attrAssertion, rules) if err != nil { - return fmt.Errorf("Plaintext policy check failed: %w", err) + return fmt.Errorf("plaintext policy check failed: %w", err) } - + case "application/vnd.in-toto+dsse": evEnv := &dsse.Envelope{} if err := json.Unmarshal(evContent, evEnv); err != nil { return err } - + evStatement, err := getStatementDSSEPayload(evEnv) if err != nil { return err } - + err = policy.ApplyAttestationRules(evStatement, attrAssertion, rules) if err != nil { - return fmt.Errorf("Attestation policy check failed: %w", err) + return fmt.Errorf("attestation policy check failed: %w", err) } - - default: - return fmt.Errorf("Evidence type not supported: %s", ev.GetMediaType()) + default: + return fmt.Errorf("evidence type not supported: %s", ev.GetMediaType()) } } @@ -241,14 +239,14 @@ func checkEvidence(cmd *cobra.Command, args []string) error { return nil } -func pbStructToSCAI(s *structpb.Struct) (*scai.AttributeReport, error) { - structJson, err := protojson.Marshal(s) +func pbStructToSCAI(s *structpb.Struct) (*scai.AttributeReport, error) { + structJSON, err := protojson.Marshal(s) if err != nil { return nil, err } report := &scai.AttributeReport{} - err = protojson.Unmarshal(structJson, report) + err = protojson.Unmarshal(structJSON, report) if err != nil { return nil, err } @@ -259,12 +257,12 @@ func pbStructToSCAI(s *structpb.Struct) (*scai.AttributeReport, error) { func getStatementDSSEPayload(envelope *dsse.Envelope) (*ita.Statement, error) { stBytes, err := envelope.DecodeB64Payload() if err != nil { - return nil, fmt.Errorf("Failed to decode DSSE payload: %w", err) + return nil, fmt.Errorf("failed to decode DSSE payload: %w", err) } - + statement := &ita.Statement{} if err = protojson.Unmarshal(stBytes, statement); err != nil { - return nil, fmt.Errorf("Failed to unmarshal Statement: %w", err) + return nil, fmt.Errorf("failed to unmarshal Statement: %w", err) } return statement, nil diff --git a/scai-gen/cmd/rd.go b/scai-gen/cmd/rd.go index 9374579..8252a1e 100644 --- a/scai-gen/cmd/rd.go +++ b/scai-gen/cmd/rd.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strings" + "github.com/in-toto/scai-demos/scai-gen/fileio" "github.com/in-toto/scai-demos/scai-gen/policy" @@ -51,8 +52,8 @@ func init() { "", "Filename to write out the JSON-encoded object", ) - rdCmd.MarkPersistentFlagRequired("out-file") - + rdCmd.MarkPersistentFlagRequired("out-file") //nolint:errcheck + rdCmd.AddCommand(rdFileCmd) rdCmd.AddCommand(rdRemoteCmd) } @@ -65,7 +66,7 @@ func init() { "", "A name for the local file", ) - + rdFileCmd.Flags().StringVarP( &uri, "uri", @@ -73,7 +74,7 @@ func init() { "", "The URI of the resource", ) - + rdFileCmd.Flags().BoolVarP( &withContent, "content", @@ -163,12 +164,11 @@ func readAnnotations(filename string) (*structpb.Struct, error) { return annotations, nil } -func genRdFromFile(cmd *cobra.Command, args []string) error { - +func genRdFromFile(_ *cobra.Command, args []string) error { filename := args[0] fileBytes, err := os.ReadFile(filename) if err != nil { - return fmt.Errorf("Error reading resource file: %w", err) + return fmt.Errorf("error reading resource file: %w", err) } var content []byte @@ -185,61 +185,60 @@ func genRdFromFile(cmd *cobra.Command, args []string) error { annotations, err := readAnnotations(annotationsFile) if err != nil { - return fmt.Errorf("Error reading annotations file: %w", err) + return fmt.Errorf("error reading annotations file: %w", err) } - + rd := &ita.ResourceDescriptor{ - Name: rdName, - Uri: uri, - Digest: map[string]string{"sha256": strings.ToLower(sha256Digest)}, - Content: content, + Name: rdName, + Uri: uri, + Digest: map[string]string{"sha256": strings.ToLower(sha256Digest)}, + Content: content, DownloadLocation: downloadLocation, - MediaType: mediaType, - Annotations: annotations, + MediaType: mediaType, + Annotations: annotations, } err = rd.Validate() if err != nil { - return fmt.Errorf("Invalid resource descriptor: %w", err) + return fmt.Errorf("invalid resource descriptor: %w", err) } - + return fileio.WritePbToFile(rd, outFile) } -func genRdForRemote(cmd *cobra.Command, args []string) error { - - remoteUri := args[0] +func genRdForRemote(_ *cobra.Command, args []string) error { + remoteURI := args[0] - var digestSet map[string]string + digestSet := make(map[string]string) if len(digest) > 0 { // the in-toto spec expects a hex-encoded string in DigestSets // https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md _, err := hex.DecodeString(digest) if err != nil { - return fmt.Errorf("Digest is not valid hex-encoded string: %w", err) + return fmt.Errorf("digest is not valid hex-encoded string: %w", err) } - + // we can assume that we have both variables set at this point digestSet = map[string]string{hashAlg: strings.ToLower(digest)} } annotations, err := readAnnotations(annotationsFile) if err != nil { - return fmt.Errorf("Error reading annotations file: %w", err) + return fmt.Errorf("error reading annotations file: %w", err) } - + rd := &ita.ResourceDescriptor{ - Name: name, - Uri: remoteUri, - Digest: digestSet, + Name: name, + Uri: remoteURI, + Digest: digestSet, DownloadLocation: downloadLocation, - Annotations: annotations, + Annotations: annotations, } err = rd.Validate() if err != nil { - return fmt.Errorf("Invalid resource descriptor: %w", err) + return fmt.Errorf("invalid resource descriptor: %w", err) } - + return fileio.WritePbToFile(rd, outFile) } diff --git a/scai-gen/cmd/report.go b/scai-gen/cmd/report.go index 8ce5961..024f179 100644 --- a/scai-gen/cmd/report.go +++ b/scai-gen/cmd/report.go @@ -2,10 +2,11 @@ package cmd import ( "fmt" + "github.com/in-toto/scai-demos/scai-gen/fileio" - ita "github.com/in-toto/attestation/go/v1" scai "github.com/in-toto/attestation/go/predicates/scai/v0" + ita "github.com/in-toto/attestation/go/v1" "github.com/spf13/cobra" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" @@ -31,8 +32,8 @@ func init() { "", "Filename to write out the JSON-encoded object", ) - reportCmd.MarkFlagRequired("out-file") - + reportCmd.MarkFlagRequired("out-file") //nolint:errcheck + reportCmd.Flags().StringVarP( &subjectFile, "subject", @@ -40,7 +41,7 @@ func init() { "", "The filename of the JSON-encoded subject resource descriptor", ) - reportCmd.MarkFlagRequired("subject") + reportCmd.MarkFlagRequired("subject") //nolint:errcheck reportCmd.Flags().StringVarP( &producerFile, @@ -51,9 +52,8 @@ func init() { ) } -func genAttrReport(cmd *cobra.Command, args []string) error { - - var attrAsserts []*scai.AttributeAssertion +func genAttrReport(_ *cobra.Command, args []string) error { + attrAsserts := make([]*scai.AttributeAssertion, 0, len(args)) for _, attrAssertPath := range args { aa := &scai.AttributeAssertion{} err := fileio.ReadPbFromFile(attrAssertPath, aa) @@ -76,12 +76,12 @@ func genAttrReport(cmd *cobra.Command, args []string) error { // first, generate the SCAI Report ar := &scai.AttributeReport{ Attributes: attrAsserts, - Producer: producer, + Producer: producer, } err := ar.Validate() if err != nil { - return fmt.Errorf("Invalid SCAI attribute report: %w", err) + return fmt.Errorf("invalid SCAI attribute report: %w", err) } // then, plug the Report into an in-toto Statement @@ -95,12 +95,12 @@ func genAttrReport(cmd *cobra.Command, args []string) error { } } - reportJson, err := protojson.Marshal(ar) + reportJSON, err := protojson.Marshal(ar) if err != nil { return err } reportStruct := &structpb.Struct{} - err = protojson.Unmarshal(reportJson, reportStruct) + err = protojson.Unmarshal(reportJSON, reportStruct) if err != nil { return err } @@ -114,8 +114,8 @@ func genAttrReport(cmd *cobra.Command, args []string) error { err = statement.Validate() if err != nil { - return fmt.Errorf("Invalid in-toto Statement: %w", err) + return fmt.Errorf("invalid in-toto Statement: %w", err) } - + return fileio.WritePbToFile(statement, outFile) } diff --git a/scai-gen/cmd/root.go b/scai-gen/cmd/root.go index 747bfb0..239e284 100644 --- a/scai-gen/cmd/root.go +++ b/scai-gen/cmd/root.go @@ -2,7 +2,7 @@ package cmd import ( "os" - + "github.com/spf13/cobra" ) @@ -14,7 +14,7 @@ var rootCmd = &cobra.Command{ var outFile string -func init() { +func init() { rootCmd.AddCommand(rdCmd) rootCmd.AddCommand(assertCmd) rootCmd.AddCommand(reportCmd) @@ -28,4 +28,4 @@ func Execute() { if err != nil { os.Exit(1) } -} \ No newline at end of file +} diff --git a/scai-gen/fileio/dsse.go b/scai-gen/fileio/dsse.go index 1a1a1d7..0d1c66f 100644 --- a/scai-gen/fileio/dsse.go +++ b/scai-gen/fileio/dsse.go @@ -1,22 +1,21 @@ package fileio -import( +import ( "encoding/json" "fmt" "os" - + ita "github.com/in-toto/attestation/go/v1" "github.com/secure-systems-lab/go-securesystemslib/dsse" "google.golang.org/protobuf/encoding/protojson" ) - func ReadDSSEFile(path string) (*dsse.Envelope, error) { envBytes, err := os.ReadFile(path) if err != nil { return nil, err } - + envelope := &dsse.Envelope{} if err := json.Unmarshal(envBytes, envelope); err != nil { return nil, err @@ -26,17 +25,16 @@ func ReadDSSEFile(path string) (*dsse.Envelope, error) { } func ReadStatementFromDSSEFile(path string) (*ita.Statement, error) { - envelope, err := ReadDSSEFile(path) if err != nil { - return nil, fmt.Errorf("Failed to read DSSE file %w", err) + return nil, fmt.Errorf("failed to read DSSE file %w", err) } - + stBytes, err := envelope.DecodeB64Payload() if err != nil { return nil, err } - + statement := &ita.Statement{} if err = protojson.Unmarshal(stBytes, statement); err != nil { return nil, err diff --git a/scai-gen/fileio/map.go b/scai-gen/fileio/map.go index 35b1202..b0c2286 100644 --- a/scai-gen/fileio/map.go +++ b/scai-gen/fileio/map.go @@ -1,8 +1,8 @@ package fileio -import( - "path/filepath" +import ( "os" + "path/filepath" ) func ReadFileIntoMap(filename string, fileMap map[string][]byte) error { @@ -11,7 +11,7 @@ func ReadFileIntoMap(filename string, fileMap map[string][]byte) error { if err != nil { return err } - + fileMap[name] = content return nil -} \ No newline at end of file +} diff --git a/scai-gen/fileio/pb.go b/scai-gen/fileio/pb.go index d0a256e..11b37b5 100644 --- a/scai-gen/fileio/pb.go +++ b/scai-gen/fileio/pb.go @@ -1,11 +1,11 @@ package fileio -import( +import ( "fmt" "os" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" ) func WritePbToFile(pb proto.Message, outFile string) error { @@ -14,18 +14,18 @@ func WritePbToFile(pb proto.Message, outFile string) error { return err } - return os.WriteFile(outFile, pbBytes, 0644) + return os.WriteFile(outFile, pbBytes, 0644) //nolint:gosec } func ReadPbFromFile(filename string, pb proto.Message) error { fileBytes, err := os.ReadFile(filename) if err != nil { - return fmt.Errorf("Error reading file: %w", err) + return fmt.Errorf("error reading file: %w", err) } err = protojson.Unmarshal(fileBytes, pb) if err != nil { - return fmt.Errorf("Error unmarshalling protobuf: %w", err) + return fmt.Errorf("error unmarshalling protobuf: %w", err) } return nil diff --git a/scai-gen/policy/attestation.go b/scai-gen/policy/attestation.go index 33feea4..f4ad120 100644 --- a/scai-gen/policy/attestation.go +++ b/scai-gen/policy/attestation.go @@ -1,11 +1,11 @@ package policy -import( - "github.com/in-toto/attestation-verifier/verifier" +import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/interpreter" - ita "github.com/in-toto/attestation/go/v1" + "github.com/in-toto/attestation-verifier/verifier" scai "github.com/in-toto/attestation/go/predicates/scai/v0" + ita "github.com/in-toto/attestation/go/v1" ) func getAttestationCELEnv() (*cel.Env, error) { @@ -30,7 +30,6 @@ func getAttestationActivation(statement *ita.Statement, attrAssertion *scai.Attr } func ApplyAttestationRules(statement *ita.Statement, attrAssertion *scai.AttributeAssertion, rules []verifier.Constraint) error { - env, err := getAttestationCELEnv() if err != nil { return err @@ -42,4 +41,4 @@ func ApplyAttestationRules(statement *ita.Statement, attrAssertion *scai.Attribu } return applyRules(env, input, rules) -} \ No newline at end of file +} diff --git a/scai-gen/policy/checks.go b/scai-gen/policy/checks.go index d4246ae..9db556b 100644 --- a/scai-gen/policy/checks.go +++ b/scai-gen/policy/checks.go @@ -1,15 +1,15 @@ package policy -import( +import ( "bytes" "crypto/sha256" "encoding/hex" "fmt" "strings" - - "github.com/in-toto/attestation-verifier/verifier" + "github.com/google/cel-go/cel" "github.com/google/cel-go/interpreter" + "github.com/in-toto/attestation-verifier/verifier" ) type SCAIEvidencePolicy struct { @@ -24,7 +24,6 @@ func GenSHA256(bytes []byte) []byte { } func MatchDigest(hexDigest string, blob []byte) bool { - digest := GenSHA256(blob) decoded, err := hex.DecodeString(hexDigest) @@ -37,9 +36,7 @@ func MatchDigest(hexDigest string, blob []byte) bool { } func applyRules(env *cel.Env, input interpreter.Activation, rules []verifier.Constraint) error { - for _, r := range rules { - ast, issues := env.Compile(r.Rule) if issues != nil && issues.Err() != nil { return fmt.Errorf("CEL compilation issues: %w", issues.Err()) @@ -57,6 +54,7 @@ func applyRules(env *cel.Env, input interpreter.Activation, rules []verifier.Con } return err } + switch result := out.Value().(type) { case bool: if !result { diff --git a/scai-gen/policy/plaintext.go b/scai-gen/policy/plaintext.go index a0cbbe9..34a9116 100644 --- a/scai-gen/policy/plaintext.go +++ b/scai-gen/policy/plaintext.go @@ -1,11 +1,11 @@ package policy -import( +import ( "fmt" - - "github.com/in-toto/attestation-verifier/verifier" + "github.com/google/cel-go/cel" "github.com/google/cel-go/interpreter" + "github.com/in-toto/attestation-verifier/verifier" scai "github.com/in-toto/attestation/go/predicates/scai/v0" ) @@ -19,22 +19,21 @@ func getPlaintextCELEnv() (*cel.Env, error) { func getPlaintextActivation(text string, attrAssertion *scai.AttributeAssertion) (interpreter.Activation, error) { return interpreter.NewActivation(map[string]any{ - "text": text, + "text": text, "assertion": attrAssertion, }) } func ApplyPlaintextRules(text string, attrAssertion *scai.AttributeAssertion, rules []verifier.Constraint) error { - env, err := getPlaintextCELEnv() if err != nil { - return fmt.Errorf("Failed to init CEL env: %w", err) + return fmt.Errorf("failed to init CEL env: %w", err) } input, err := getPlaintextActivation(text, attrAssertion) if err != nil { - return fmt.Errorf("Failed to get CEL activation: %w", err) + return fmt.Errorf("failed to get CEL activation: %w", err) } return applyRules(env, input, rules) -} \ No newline at end of file +}