-
-
Notifications
You must be signed in to change notification settings - Fork 982
Add Attestation Repair action In Avo #6186
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
+887
−0
Merged
Changes from 1 commit
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
bf85b70
Implement RepairAttestation action and related functionality for atte…
colby-swandale c5bea32
Update app/avo/resources/attestation.rb
colby-swandale fd33688
Enhance attestation repair functionality and tests
colby-swandale 4994871
Update kindVersion to hashedrekord in attestation handling and tests
colby-swandale 1637ade
Enhance attestation repair functionality to provide detailed issue re…
colby-swandale 0664391
Update valid_bundle? method to include repairable check and add sigst…
colby-swandale fb17511
Add mock test data for certificate handling in attestation tests
colby-swandale f966f92
Add mock test data for attestation handling in repair tests
colby-swandale 65ee637
remove lgmt linting comments
colby-swandale 7385ecd
Filter CodeQL results from tests
colby-swandale d381cf4
Add avo action to perform online validation of a gem attestion aginst…
colby-swandale 1bbf52d
resolve rubocop errors
colby-swandale 3f64271
Resolve outstanding issues identified by Claude
colby-swandale 3f8ff16
Add happy path test for the validate attestation PR
colby-swandale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| class Avo::Actions::RepairAttestation < Avo::Actions::ApplicationAction | ||
| self.name = "Repair attestation" | ||
| self.visible = lambda { | ||
| current_user.team_member?("rubygems-org") && view == :show | ||
| } | ||
| self.message = lambda { | ||
| if resource.record.repairable? | ||
| "This attestation has invalid data that needs repair. Proceed with repair?" | ||
| else | ||
| "This attestation appears valid. No repairs are expected." | ||
| end | ||
| } | ||
| self.confirm_button_label = "Repair attestation" | ||
|
|
||
| class ActionHandler < Avo::Actions::ActionHandler | ||
| def handle_record(attestation) | ||
| changes = attestation.repair! | ||
|
|
||
| if changes | ||
| succeed "Attestation repaired: #{changes.join(', ')}" | ||
| else | ||
| succeed "No repair was needed" | ||
| end | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| module AttestationBundleRepair | ||
| extend ActiveSupport::Concern | ||
|
|
||
| def repairable? | ||
| verification = body["verificationMaterial"] | ||
| return false if verification.blank? | ||
|
|
||
| missing_kind_version?(verification) || double_encoded_certificate?(verification) | ||
| end | ||
|
|
||
| def repair! | ||
| verification = body["verificationMaterial"] | ||
| return false if verification.blank? | ||
|
|
||
| new_body = body.deep_dup | ||
| new_verification = new_body["verificationMaterial"] | ||
| changes = [] | ||
|
|
||
| repair_kind_version!(new_verification, changes) if missing_kind_version?(verification) | ||
| repair_certificate!(new_verification, changes) if double_encoded_certificate?(verification) | ||
|
|
||
| return false if changes.empty? | ||
|
|
||
| update!(body: new_body) | ||
| changes | ||
| end | ||
colby-swandale marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private | ||
|
|
||
| def missing_kind_version?(verification) | ||
| verification["tlogEntries"]&.any? { |entry| !entry.key?("kindVersion") } | ||
| end | ||
|
|
||
| def double_encoded_certificate?(verification) | ||
| return false unless (raw_bytes = verification.dig("certificate", "rawBytes")) | ||
|
|
||
| decoded = Base64.strict_decode64(raw_bytes) | ||
| decoded.start_with?("-----BEGIN CERTIFICATE-----") | ||
| rescue ArgumentError | ||
| false | ||
| end | ||
|
|
||
| def repair_kind_version!(verification, changes) | ||
colby-swandale marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| verification["tlogEntries"]&.each_with_index do |entry, idx| | ||
| next if entry.key?("kindVersion") | ||
| entry["kindVersion"] = { "kind" => "dsse", "version" => "0.0.1" } | ||
colby-swandale marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| changes << "Added missing kindVersion to tlogEntry #{idx}" | ||
| end | ||
| end | ||
|
|
||
| def repair_certificate!(verification, changes) | ||
| raw_bytes = verification.dig("certificate", "rawBytes") | ||
| return unless raw_bytes | ||
|
|
||
| decoded = Base64.strict_decode64(raw_bytes) | ||
| return unless decoded.start_with?("-----BEGIN CERTIFICATE-----") | ||
|
|
||
| cert = OpenSSL::X509::Certificate.new(decoded) | ||
| verification["certificate"]["rawBytes"] = Base64.strict_encode64(cert.to_der) | ||
| changes << "Converted double-encoded PEM certificate to DER" | ||
| rescue ArgumentError, OpenSSL::X509::CertificateError => e | ||
| Rails.logger.warn("Failed to repair certificate: #{e.message}") | ||
colby-swandale marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| require "test_helper" | ||
|
|
||
| class RepairAttestationTest < ActiveSupport::TestCase | ||
| make_my_diffs_pretty! | ||
|
|
||
| setup do | ||
| @view_context = mock | ||
| @avo = mock | ||
| @view_context.stubs(:avo).returns(@avo) | ||
| @avo.stubs(:resources_audit_path).returns("resources_audit_path") | ||
| Avo::Current.stubs(:view_context).returns(@view_context) | ||
| @admin = create(:admin_github_user, :is_admin) | ||
| @version = create(:version) | ||
| end | ||
|
|
||
| test "repairs attestation with missing kindVersion" do | ||
| attestation = Attestation.create!( | ||
| version: @version, | ||
| media_type: "application/vnd.dev.sigstore.bundle.v0.3+json", | ||
| body: { | ||
| "verificationMaterial" => { | ||
| "tlogEntries" => [{ "logIndex" => 123 }], | ||
| "certificate" => { "rawBytes" => Base64.strict_encode64("DER data") } | ||
| } | ||
| } | ||
|
||
| ) | ||
|
|
||
| assert_predicate attestation, :repairable? | ||
|
|
||
| action = Avo::Actions::RepairAttestation.new | ||
| action.handle( | ||
| fields: { comment: "Repairing invalid attestation" }, | ||
| current_user: @admin, | ||
| resource: nil, | ||
| records: [attestation], | ||
| query: nil | ||
| ) | ||
|
|
||
| attestation.reload | ||
|
|
||
| refute_predicate attestation, :repairable? | ||
| assert_equal( | ||
| { "kind" => "dsse", "version" => "0.0.1" }, | ||
| attestation.body.dig("verificationMaterial", "tlogEntries", 0, "kindVersion") | ||
| ) | ||
| end | ||
colby-swandale marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| test "repairs attestation with double-encoded PEM certificate" do | ||
| key = OpenSSL::PKey::RSA.new(2048) | ||
| cert = OpenSSL::X509::Certificate.new | ||
| cert.version = 2 | ||
| cert.serial = 1 | ||
| cert.subject = OpenSSL::X509::Name.parse("/CN=Test") | ||
| cert.issuer = cert.subject | ||
| cert.public_key = key.public_key | ||
| cert.not_before = Time.current | ||
| cert.not_after = Time.current + 3600 | ||
| cert.sign(key, OpenSSL::Digest.new("SHA256")) | ||
|
|
||
| pem_cert = cert.to_pem | ||
| double_encoded = Base64.strict_encode64(pem_cert) | ||
|
|
||
| attestation = Attestation.create!( | ||
| version: @version, | ||
| media_type: "application/vnd.dev.sigstore.bundle.v0.3+json", | ||
| body: { | ||
| "verificationMaterial" => { | ||
| "tlogEntries" => [{ "kindVersion" => { "kind" => "dsse", "version" => "0.0.1" } }], | ||
| "certificate" => { "rawBytes" => double_encoded } | ||
| } | ||
| } | ||
|
||
| ) | ||
|
|
||
| assert_predicate attestation, :repairable? | ||
|
|
||
| action = Avo::Actions::RepairAttestation.new | ||
| action.handle( | ||
| fields: { comment: "Repairing invalid attestation" }, | ||
| current_user: @admin, | ||
| resource: nil, | ||
| records: [attestation], | ||
| query: nil | ||
| ) | ||
|
|
||
| attestation.reload | ||
|
|
||
| refute_predicate attestation, :repairable? | ||
|
|
||
| raw_bytes = attestation.body.dig("verificationMaterial", "certificate", "rawBytes") | ||
| decoded = Base64.strict_decode64(raw_bytes) | ||
|
|
||
| refute decoded.start_with?("-----BEGIN CERTIFICATE-----") | ||
| end | ||
colby-swandale marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| test "does nothing for attestation without known issues" do | ||
| attestation = Attestation.create!( | ||
| version: @version, | ||
| media_type: "application/vnd.dev.sigstore.bundle.v0.3+json", | ||
| body: { | ||
| "verificationMaterial" => { | ||
| "tlogEntries" => [{ "kindVersion" => { "kind" => "dsse", "version" => "0.0.1" } }], | ||
| "certificate" => { "rawBytes" => Base64.strict_encode64("DER data") } | ||
| } | ||
| } | ||
|
||
| ) | ||
|
|
||
| refute_predicate attestation, :repairable? | ||
| original_body = attestation.body.deep_dup | ||
|
|
||
| action = Avo::Actions::RepairAttestation.new | ||
| action.handle( | ||
| fields: { comment: "Attempting repair on valid attestation" }, | ||
| current_user: @admin, | ||
| resource: nil, | ||
| records: [attestation], | ||
| query: nil | ||
| ) | ||
|
|
||
| attestation.reload | ||
|
|
||
| assert_equal original_body, attestation.body | ||
| end | ||
|
|
||
| test "repairs both missing kindVersion and double-encoded certificate simultaneously" do | ||
| key = OpenSSL::PKey::RSA.new(2048) | ||
| cert = OpenSSL::X509::Certificate.new | ||
| cert.version = 2 | ||
| cert.serial = 1 | ||
| cert.subject = OpenSSL::X509::Name.parse("/CN=Test") | ||
| cert.issuer = cert.subject | ||
| cert.public_key = key.public_key | ||
| cert.not_before = Time.current | ||
| cert.not_after = Time.current + 3600 | ||
| cert.sign(key, OpenSSL::Digest.new("SHA256")) | ||
|
|
||
| pem_cert = cert.to_pem | ||
| double_encoded = Base64.strict_encode64(pem_cert) | ||
|
|
||
| attestation = Attestation.create!( | ||
| version: @version, | ||
| media_type: "application/vnd.dev.sigstore.bundle.v0.3+json", | ||
| body: { | ||
| "verificationMaterial" => { | ||
| "tlogEntries" => [{ "logIndex" => 123 }], | ||
| "certificate" => { "rawBytes" => double_encoded } | ||
| } | ||
| } | ||
|
||
| ) | ||
|
|
||
| assert_predicate attestation, :repairable? | ||
|
|
||
| action = Avo::Actions::RepairAttestation.new | ||
| action.handle( | ||
| fields: { comment: "Repairing attestation with both issues" }, | ||
| current_user: @admin, | ||
| resource: nil, | ||
| records: [attestation], | ||
| query: nil | ||
| ) | ||
|
|
||
| attestation.reload | ||
|
|
||
| refute_predicate attestation, :repairable? | ||
|
|
||
| # Verify kindVersion was added | ||
| assert_equal( | ||
| { "kind" => "dsse", "version" => "0.0.1" }, | ||
| attestation.body.dig("verificationMaterial", "tlogEntries", 0, "kindVersion") | ||
| ) | ||
|
|
||
| # Verify certificate was converted to DER | ||
| raw_bytes = attestation.body.dig("verificationMaterial", "certificate", "rawBytes") | ||
| decoded = Base64.strict_decode64(raw_bytes) | ||
|
|
||
| refute decoded.start_with?("-----BEGIN CERTIFICATE-----") | ||
| end | ||
| end | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.