Skip to content

refactor(server): change custom advisory and vulnerability data types fr… #8923

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 30, 2025

Conversation

tonaim
Copy link
Collaborator

@tonaim tonaim commented May 26, 2025

  1. Regenerated gRPC code (service.pb.go):
    Updated all getters, raw descriptors, dependency indexes, and field types to use []byte instead of *structpb.Value.
  2. Conversion logic (pkg/rpc/convert.go):
    Encoding (ConvertToRPCVulns): JSON-marshal both vuln.Custom and vuln.Vulnerability.Custom into the new byte fields.
    Decoding (ConvertFromRPCVulns): JSON-unmarshal incoming byte slices back into interface{}, falling back to nil on unmarshal errors.
  3. Unit tests:
    Updated existing tests to expect raw byte slices for non-empty custom data and nil interfaces for empty data.
    Added new round-trip tests to verify that custom fields serialize → deserialize correctly.

Why This Matters

  • Performance & Dependency Reduction
    Removing structpb.Value avoids heavy reflection and descriptor overhead, streamlining serialization on both server and client.

  • Data Fidelity
    Callers now get the exact original structure (maps, arrays, strings) without needing to re-interpret a generic Protobuf Value wrapper.

  • more cross-language-friendly
    The generated stub contains a plain bytes field, which every language’s core protobuf runtime already knows how to handle. No Struct/Value helpers required.

  • Test Consistency
    Ensures empty custom data fields yield a true Go nil interface, matching existing test expectations.

Checklist

  • I've read the guidelines for contributing to this repository.
  • I've followed the conventions in the PR title.
  • I've added tests that prove my fix is effective or that my feature works.
  • I've updated the documentation with the relevant information (if needed).
  • I've added usage information (if the PR introduces new options)
  • I've included a "before" and "after" example to the description (if the PR is a user interface change).

…om structpb.Value to bytes for improved serialization
@tonaim tonaim requested a review from knqyf263 as a code owner May 26, 2025 18:53
@tonaim tonaim marked this pull request as draft May 26, 2025 18:54
@tonaim tonaim changed the title refactor(rpc): change custom advisory and vulnerability data types fr… refactor(server): change custom advisory and vulnerability data types fr… May 26, 2025
@tonaim tonaim marked this pull request as ready for review May 26, 2025 20:12
@@ -595,6 +598,18 @@ func ConvertFromRPCVulns(rpcVulns []*common.Vulnerability) []types.DetectedVulne
publishedDate = lo.ToPtr(vuln.PublishedDate.AsTime())
}

var cVData, cAData any
if len(vuln.CustomVulnData) > 0 {
if err := json.Unmarshal(vuln.CustomVulnData, &cVData); err != nil {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this additional unmarshal? cannt we directly assign customAdvisoryData to proto attribute.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deven0t , handled it now.

@knqyf263 knqyf263 requested a review from Copilot May 27, 2025 05:15
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Refactor serialization of custom advisory and vulnerability data to use raw bytes instead of structpb.Value, updating protobuf definitions, generated code, and conversion logic.

  • Changed custom_advisory_data and custom_vuln_data fields in the Vulnerability message from google.protobuf.Value to bytes.
  • Regenerated service.pb.go to reflect byte-slice fields, updated getters, raw descriptors, and dependency indexes.
  • Updated ConvertToRPCVulns/ConvertFromRPCVulns to marshal custom data as JSON bytes and assign raw bytes on the return path.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
rpc/common/service.proto Updated custom data fields in Vulnerability to bytes
rpc/common/service.pb.go Regenerated code for byte-slice fields, getters, and descriptors
pkg/rpc/convert.go Switched to JSON marshalling and adapted conversion functions
Comments suppressed due to low confidence (1)

pkg/rpc/convert.go:303

  • There are no unit tests covering the new JSON serialization logic for custom advisory and vulnerability data. Add tests to verify that data round-trips correctly through ConvertToRPCVulns and ConvertFromRPCVulns.
jsonBytes, _ := json.Marshal(vuln.Custom) // nolint: errcheck

Comment on lines 603 to 607
vulnCustom = any(vuln.CustomVulnData)
}
var advisoryCustom any
if len(vuln.CustomAdvisoryData) > 0 {
advisoryCustom = any(vuln.CustomAdvisoryData)
Copy link
Preview

Copilot AI May 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConvertFromRPCVulns assigns raw JSON bytes to the Custom field instead of unmarshalling back into a structured object, so callers will receive []byte rather than the original data. Consider using json.Unmarshal to decode vuln.CustomVulnData into an interface{} before assignment.

Suggested change
vulnCustom = any(vuln.CustomVulnData)
}
var advisoryCustom any
if len(vuln.CustomAdvisoryData) > 0 {
advisoryCustom = any(vuln.CustomAdvisoryData)
if err := json.Unmarshal(vuln.CustomVulnData, &vulnCustom); err != nil {
log.Logger.Warnf("Failed to unmarshal CustomVulnData: %v", err)
}
}
var advisoryCustom any
if len(vuln.CustomAdvisoryData) > 0 {
if err := json.Unmarshal(vuln.CustomAdvisoryData, &advisoryCustom); err != nil {
log.Logger.Warnf("Failed to unmarshal CustomAdvisoryData: %v", err)
}

Copilot uses AI. Check for mistakes.

Comment on lines 136 to +137
google.protobuf.Timestamp last_modified_date = 16;
google.protobuf.Value custom_advisory_data = 17;
google.protobuf.Value custom_vuln_data = 18;
bytes custom_advisory_data = 17;
Copy link
Preview

Copilot AI May 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding a comment above these fields to clarify that they now carry JSON-encoded data as raw bytes, and clients must JSON-decode them to retrieve the original structure.

Copilot uses AI. Check for mistakes.

Copy link
Collaborator

@knqyf263 knqyf263 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the PR description so we can understand why this change is needed.

@tonaim
Copy link
Collaborator Author

tonaim commented May 27, 2025

Please update the PR description so we can understand why this change is needed.

Updated the description details.

Remove redundant type conversions where values are already of the correct type,
simplifying the code and improving readability.
@knqyf263
Copy link
Collaborator

Language Agnostic
Raw JSON bytes are easily consumed by non-Go clients without importing Protobuf Struct libraries.

Are you sure it's language agnostic? We use protobuf anyway.

Removing structpb.Value avoids heavy reflection and descriptor overhead, streamlining serialization on both server and client.

This PR is just migrating to json.Marshal. It should explain why it improves performance.

@tonaim
Copy link
Collaborator Author

tonaim commented May 29, 2025

Language Agnostic
Raw JSON bytes are easily consumed by non-Go clients without importing Protobuf Struct libraries.

Are you sure it's language agnostic? We use protobuf anyway.

You’re right that the envelope is still a protobuf Vulnerability message, so every consumer will continue to rely on Protobuf‐generated stubs.
The point I was trying to make is that parsing the custom blob itself no longer requires the extra Protobuf helper types (google.protobuf.Struct|Value) on the client side.

Removing structpb.Value avoids heavy reflection and descriptor overhead, streamlining serialization on both server and client.

This PR is just migrating to json.Marshal. It should explain why it improves performance.

So, what was slow with structpb.NewStruct
structpb.NewStruct recursively reflects over every element to wrap it in *structpb.Value allocations.
Reflection in Go is orders of magnitude slower than regular marshaling and adds a lot of heap churn. Found a online article .

To support that having []byte is advantageous, I wrote small benchmarking program.

var oneCustom = map[string]interface{}{
	"id":       "CVE-2025-1234",
	"severity": "HIGH",
	"vendor": map[string]interface{}{
		"name":    "Acme Corp",
		"patched": true,
	},
	"cvss": map[string]interface{}{
		"score": 9.8,
		"vector": map[string]interface{}{
			"attackVector": "NETWORK",
			"scope":        "CHANGED",
		},
	},
}

// Generate **10,000** entries to simulate Trivy scan volume
var dataset = make([]map[string]interface{}, 10000)

func init() {
	for i := 0; i < len(dataset); i++ {
		// You can also clone oneCustom to avoid sharing pointers, but for benchmark it's OK
		dataset[i] = oneCustom
	}
}

// Old method: structpb.NewStruct
func BenchmarkStructPB_Large(b *testing.B) {
	for i := 0; i < b.N; i++ {
		for _, entry := range dataset {
			_, err := structpb.NewStruct(entry)
			if err != nil {
				b.Fatal(err)
			}
		}
	}
}

// New method: json.Marshal
func BenchmarkMarshalBytes_Large(b *testing.B) {
	for i := 0; i < b.N; i++ {
		for _, entry := range dataset {
			_, err := json.Marshal(entry)
			if err != nil {
				b.Fatal(err)
			}
		}
	}
}
go test -bench=. -benchmem -run=^$ benchmark/bench_convert_test.go

BenchmarkStructPB_Large-10         2     71721300 ns/op   36098240 B/op  470122 allocs/op
BenchmarkMarshalBytes_Large-10     3     41519400 ns/op   16429312 B/op  240230 allocs/op

@knqyf263
Copy link
Collaborator

knqyf263 commented May 29, 2025

Raw JSON bytes are easily consumed by non-Go clients without importing Protobuf Struct libraries.

We still need a protobuf library to parse the HTTP response. Could you please explain in more detail what this means?

The point I was trying to make is that parsing the custom blob itself no longer requires the extra Protobuf helper types (google.protobuf.Struct|Value) on the client side.

If this is what you mean, the above explanatin doesn't seem accurate.

@tonaim
Copy link
Collaborator Author

tonaim commented May 29, 2025

We still need a protobuf library to parse the HTTP response. Could you please explain in more detail what this means?

You’re absolutely right that the outer envelope remains a normal protobuf Vulnerability message, so any client that speaks gRPC will keep using its protobuf runtime to read the response.

The generated stub contains a plain bytes field, which every language’s core protobuf runtime already knows how to handle. No Struct/Value helpers required.

I am rephrasing the description

@knqyf263 knqyf263 added this pull request to the merge queue May 30, 2025
Merged via the queue into aquasecurity:main with commit c29bb21 May 30, 2025
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants