Skip to content

Commit d0d3035

Browse files
StantonMattsimi
authored andcommitted
Fix GitHub advisory YAML indentation
Signed-off-by: Matthew Stanton <stantonmatthewj@gmail.com>
1 parent 7dfa2f0 commit d0d3035

2 files changed

Lines changed: 144 additions & 3 deletions

File tree

lib/github_advisory_sync.rb

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,73 @@ def initialize(advisory)
250250
@vulnerabilities = []
251251
end
252252

253+
def self.formatted_yaml(data)
254+
yaml = data.to_yaml
255+
formatted_yaml = indent_mapping_sequences(yaml)
256+
257+
return formatted_yaml if yaml_round_trips?(formatted_yaml, data)
258+
259+
yaml
260+
end
261+
262+
def self.indent_mapping_sequences(yaml)
263+
active_sequence_indents = []
264+
pending_sequence_indent = nil
265+
block_scalar_indent = nil
266+
267+
yaml.lines.map do |line|
268+
if line.strip.empty? || line.start_with?("---")
269+
next line
270+
end
271+
272+
indent = line[/\A */].length
273+
274+
if block_scalar_indent
275+
if indent > block_scalar_indent
276+
next "#{' ' * active_sequence_indents.length}#{line}"
277+
end
278+
279+
block_scalar_indent = nil
280+
end
281+
282+
sequence_item = line.match?(/\A\s*-\s/)
283+
284+
active_sequence_indents.pop while active_sequence_indents.any? &&
285+
indent <= active_sequence_indents.last &&
286+
!(sequence_item && indent == active_sequence_indents.last)
287+
288+
if sequence_item && pending_sequence_indent == indent
289+
active_sequence_indents << indent
290+
end
291+
292+
pending_sequence_indent = sequence_indent_after_mapping_key(line, indent)
293+
block_scalar_indent = indent if block_scalar_header?(line)
294+
295+
"#{' ' * active_sequence_indents.length}#{line}"
296+
end.join
297+
end
298+
299+
def self.sequence_indent_after_mapping_key(line, indent)
300+
if line.match?(/\A\s*-\s+.*:\s*\z/)
301+
indent + 2
302+
elsif line.match?(/\A\s*[^#].*:\s*\z/)
303+
indent
304+
end
305+
end
306+
307+
def self.block_scalar_header?(line)
308+
line.match?(/\A\s*(?:-\s+)?[^#]+:\s*[>|](?:[+-]?[1-9]|[1-9][+-]?)?\s*\z/)
309+
end
310+
311+
def self.yaml_round_trips?(yaml, data)
312+
YAML.safe_load(yaml, permitted_classes: [Date]) == data
313+
rescue Psych::Exception
314+
false
315+
end
316+
317+
private_class_method :indent_mapping_sequences, :sequence_indent_after_mapping_key,
318+
:block_scalar_header?, :yaml_round_trips?
319+
253320
def identifier_list
254321
advisory["identifiers"]
255322
end
@@ -338,7 +405,7 @@ def update(package)
338405
return if saved_data == new_data
339406

340407
File.open(package.filename, 'w') do |file|
341-
file.write YAML.dump(new_data)
408+
file.write self.class.formatted_yaml(new_data)
342409
end
343410

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

433500
# The data we just wrote is incomplete,
434501
# and therefore should not be committed as is
@@ -448,7 +515,7 @@ def create(package)
448515
# Still it should be removed before the data goes into rubysec
449516
file.write "# GitHub advisory data below - **Remove this data before committing**\n"
450517
file.write "# Use this data to write patched_versions (and potentially unaffected_versions) above\n"
451-
file.write advisory.merge("vulnerabilities" => vulnerabilities).to_yaml
518+
file.write self.class.formatted_yaml(advisory.merge("vulnerabilities" => vulnerabilities))
452519
end
453520
puts "Wrote: #{filename_to_write}"
454521
filename_to_write

spec/github_advisory_sync_spec.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
require "spec_helper"
2+
require "github_advisory_sync"
3+
4+
RSpec.describe GitHub::GitHubAdvisory do
5+
describe ".formatted_yaml" do
6+
it "indents generated sequence values under their keys" do
7+
data = {
8+
"patched_versions" => [">= 3.0.1"],
9+
"related" => {
10+
"url" => [
11+
"https://github.com/autolab/Autolab/security/advisories/GHSA-v46j-h43h-rwrm"
12+
]
13+
}
14+
}
15+
16+
yaml = described_class.formatted_yaml(data)
17+
18+
expect(yaml).to include(%(patched_versions:\n - ">= 3.0.1"\n))
19+
expect(yaml).to include(
20+
"related:\n" \
21+
" url:\n" \
22+
" - https://github.com/autolab/Autolab/security/advisories/GHSA-v46j-h43h-rwrm\n"
23+
)
24+
expect(YAML.safe_load(yaml)).to eq(data)
25+
end
26+
27+
it "keeps nested array payloads valid" do
28+
data = {
29+
"description" => "Impact:\n- user-provided bullet\n",
30+
"notes" => " heading:\n - keep literal bullet\n",
31+
"vulnerabilities" => [
32+
{
33+
"package" => {
34+
"name" => "autolab"
35+
},
36+
"identifiers" => [
37+
{
38+
"type" => "CVE",
39+
"value" => "CVE-2026-1234"
40+
}
41+
]
42+
}
43+
]
44+
}
45+
46+
yaml = described_class.formatted_yaml(data)
47+
48+
expect(yaml).to include(
49+
"vulnerabilities:\n" \
50+
" - package:\n" \
51+
" name: autolab\n" \
52+
" identifiers:\n" \
53+
" - type: CVE\n" \
54+
" value: CVE-2026-1234\n"
55+
)
56+
expect(YAML.safe_load(yaml)).to eq(data)
57+
end
58+
59+
it "does not corrupt multiline quoted scalar payloads" do
60+
data = {
61+
"vulnerabilities" => [
62+
{
63+
"desc" => "x\n",
64+
"fixed" => true
65+
}
66+
]
67+
}
68+
69+
yaml = described_class.formatted_yaml(data)
70+
71+
expect(YAML.safe_load(yaml)).to eq(data)
72+
end
73+
end
74+
end

0 commit comments

Comments
 (0)