Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions lib/github_advisory_sync.rb
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,73 @@ def initialize(advisory)
@vulnerabilities = []
end

def self.formatted_yaml(data)
yaml = data.to_yaml
formatted_yaml = indent_mapping_sequences(yaml)

return formatted_yaml if yaml_round_trips?(formatted_yaml, data)

yaml
end

def self.indent_mapping_sequences(yaml)
active_sequence_indents = []
pending_sequence_indent = nil
block_scalar_indent = nil

yaml.lines.map do |line|
if line.strip.empty? || line.start_with?("---")
next line
end

indent = line[/\A */].length

if block_scalar_indent
if indent > block_scalar_indent
next "#{' ' * active_sequence_indents.length}#{line}"
end

block_scalar_indent = nil
end

sequence_item = line.match?(/\A\s*-\s/)

active_sequence_indents.pop while active_sequence_indents.any? &&
indent <= active_sequence_indents.last &&
!(sequence_item && indent == active_sequence_indents.last)

if sequence_item && pending_sequence_indent == indent
active_sequence_indents << indent
end

pending_sequence_indent = sequence_indent_after_mapping_key(line, indent)
block_scalar_indent = indent if block_scalar_header?(line)

"#{' ' * active_sequence_indents.length}#{line}"
end.join
end

def self.sequence_indent_after_mapping_key(line, indent)
if line.match?(/\A\s*-\s+.*:\s*\z/)
indent + 2
elsif line.match?(/\A\s*[^#].*:\s*\z/)
indent
end
end

def self.block_scalar_header?(line)
line.match?(/\A\s*(?:-\s+)?[^#]+:\s*[>|](?:[+-]?[1-9]|[1-9][+-]?)?\s*\z/)
end

def self.yaml_round_trips?(yaml, data)
YAML.safe_load(yaml, permitted_classes: [Date]) == data
rescue Psych::Exception
false
end

private_class_method :indent_mapping_sequences, :sequence_indent_after_mapping_key,
:block_scalar_header?, :yaml_round_trips?

def identifier_list
advisory["identifiers"]
end
Expand Down Expand Up @@ -338,7 +405,7 @@ def update(package)
return if saved_data == new_data

File.open(package.filename, 'w') do |file|
file.write YAML.dump(new_data)
file.write self.class.formatted_yaml(new_data)
end

puts "Updated: #{package.filename}"
Expand Down Expand Up @@ -428,7 +495,7 @@ def create(package)
FileUtils.mkdir_p(File.dirname(filename_to_write))
File.open(filename_to_write, "w") do |file|
# create an automatically generated advisory yaml file
file.write new_data.to_yaml
file.write self.class.formatted_yaml(new_data)

# The data we just wrote is incomplete,
# and therefore should not be committed as is
Expand All @@ -448,7 +515,7 @@ def create(package)
# Still it should be removed before the data goes into rubysec
file.write "# GitHub advisory data below - **Remove this data before committing**\n"
file.write "# Use this data to write patched_versions (and potentially unaffected_versions) above\n"
file.write advisory.merge("vulnerabilities" => vulnerabilities).to_yaml
file.write self.class.formatted_yaml(advisory.merge("vulnerabilities" => vulnerabilities))
end
puts "Wrote: #{filename_to_write}"
filename_to_write
Expand Down
74 changes: 74 additions & 0 deletions spec/github_advisory_sync_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require "spec_helper"
require "github_advisory_sync"

RSpec.describe GitHub::GitHubAdvisory do
describe ".formatted_yaml" do
it "indents generated sequence values under their keys" do
data = {
"patched_versions" => [">= 3.0.1"],
"related" => {
"url" => [
"https://github.com/autolab/Autolab/security/advisories/GHSA-v46j-h43h-rwrm"
]
}
}

yaml = described_class.formatted_yaml(data)

expect(yaml).to include(%(patched_versions:\n - ">= 3.0.1"\n))
expect(yaml).to include(
"related:\n" \
" url:\n" \
" - https://github.com/autolab/Autolab/security/advisories/GHSA-v46j-h43h-rwrm\n"
)
expect(YAML.safe_load(yaml)).to eq(data)
end

it "keeps nested array payloads valid" do
data = {
"description" => "Impact:\n- user-provided bullet\n",
"notes" => " heading:\n - keep literal bullet\n",
"vulnerabilities" => [
{
"package" => {
"name" => "autolab"
},
"identifiers" => [
{
"type" => "CVE",
"value" => "CVE-2026-1234"
}
]
}
]
}

yaml = described_class.formatted_yaml(data)

expect(yaml).to include(
"vulnerabilities:\n" \
" - package:\n" \
" name: autolab\n" \
" identifiers:\n" \
" - type: CVE\n" \
" value: CVE-2026-1234\n"
)
expect(YAML.safe_load(yaml)).to eq(data)
end

it "does not corrupt multiline quoted scalar payloads" do
data = {
"vulnerabilities" => [
{
"desc" => "x\n",
"fixed" => true
}
]
}

yaml = described_class.formatted_yaml(data)

expect(YAML.safe_load(yaml)).to eq(data)
end
end
end