Skip to content

Commit

Permalink
Add connector for Ubuntu Security Api
Browse files Browse the repository at this point in the history
  • Loading branch information
m-bucher committed Oct 28, 2024
1 parent 6973881 commit 031939a
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ group :development, optional: true do
end

gem "feedjira", "~> 3.2", group: :monitor

gem "faraday", "~> 2.12", group: :monitor
11 changes: 11 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ GEM
crass (1.0.6)
diff-lcs (1.5.1)
docile (1.4.1)
faraday (2.12.0)
faraday-net_http (>= 2.0, < 3.4)
json
logger
faraday-net_http (3.3.0)
net-http
feedjira (3.2.3)
loofah (>= 2.3.1, < 3)
sax-machine (>= 1.0, < 2)
Expand All @@ -17,10 +23,13 @@ GEM
json-stream (1.0.0)
json-streamer (2.1.0)
json-stream
logger (1.6.1)
loofah (2.23.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
method_source (1.1.0)
net-http (0.4.1)
uri
nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4)
parallel (1.20.1)
Expand Down Expand Up @@ -78,13 +87,15 @@ GEM
test-unit (3.6.2)
power_assert
unicode-display_width (2.6.0)
uri (0.13.1)

PLATFORMS
x86_64-linux

DEPENDENCIES
byebug (~> 11.1)
bzip2-ffi (~> 1.0)
faraday (~> 2.12)
feedjira (~> 3.2)
json-streamer (~> 2.1)
parallel (~> 1.20, < 1.21)
Expand Down
72 changes: 72 additions & 0 deletions lib/ubuntu_security_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require 'faraday'
require 'date'

# API doc: https://ubuntu.com/security/api/docs
class UbuntuSecurityApi
attr_accessor :retry

def initialize(opts={})
@base_url = 'https://ubuntu.com'
@retry = opts[:retry]
end

def yesterdays_erratum(release=nil)
# ASSUMPTION: among the latest 5 USNs there should be at least one older than one day
res = latest_errata(release, 5).select { |usn| Date.parse(usn['published']) < Date.today }

raise 'Could not find USN older than one day among the newest 5' if res.empty?

res.first
end

def latest_errata(release=nil, limit=nil)
opts = {
order: 'newest',
show_hidden: false
}
opts[:release] = release unless release.nil?

opts[:limit] = limit unless limit.nil?

attempt = @retry
begin
res = conn.get('/security/notices.json', opts)
res.body['notices']
rescue Faraday::ServerError
raise if attempt.nil? || attempt.zero?

attempt -= 1
retry
end
end

def latest_erratum(release=nil)
latest_errata(release, 1).first
end

private

def conn
@conn ||= Faraday.new(
url: @base_url,
headers: { 'Content-Type' => 'application/json' }
) do |builder|
# Sets the Content-Type header to application/json on each request.
# Also, if the request body is a Hash, it will automatically be encoded as JSON.
builder.request :json

# Parses JSON response bodies.
# If the response body is not valid JSON, it will raise a Faraday::ParsingError.
builder.response :json

# Raises an error on 4xx and 5xx responses.
builder.response :raise_error

# Logs requests and responses.
# By default, it only logs the request method and URL, and the request/response headers.
# builder.response :logger
end
end
end
91 changes: 91 additions & 0 deletions spec/lib/ubuntu_security_api_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

require 'spec_helper'
require './lib/ubuntu_security_api'

RSpec.describe UbuntuSecurityApi do
let(:usa) { described_class.new(retry: 3) }

context 'when get' do
let(:conn) { instance_double(Faraday::Connection) }
let(:notice) { { 'id' => 'USN-1337-1', 'published' => '2024-10-23T19:45:29.964Z' } }
let(:notices) do
[
notice,
{ 'id' => 'USN-1237-1', 'published' => '2024-10-22T18:45:29.964Z' },
{ 'id' => 'USN-1236-1', 'published' => '2024-10-22T17:45:29.964Z' },
{ 'id' => 'USN-1235-1', 'published' => '2024-10-22T16:45:29.964Z' },
{ 'id' => 'USN-1234-1', 'published' => '2024-10-22T15:45:29.964Z' }
]
end
let(:response) { instance_double(Faraday::Response) }

before do
allow(usa).to receive(:conn).and_return(conn)
allow(conn).to receive(:get).and_return(response)
allow(response).to receive(:body).and_return({ 'notices' => notices })
end

it 'latest errata' do
expect(usa.latest_errata).to eq(notices)
end

it 'latest n errata' do
allow(response).to receive(:body).and_return({ 'notices' => notices.append({ 'id' => 'too_old' }) })
expect(usa.latest_errata(nil, 5)).to eq(notices)
end

it 'latest release erratua' do
usa.latest_errata('bionic')
expect(conn).to have_received(:get).with('/security/notices.json',
{ release: 'bionic',
order: 'newest',
show_hidden: false })
end

it 'latest erratum' do
allow(response).to receive(:body).and_return({ 'notices' => notices })

expect(usa.latest_erratum).to eq(notice)
end

it "yesterday's erratum" do
notice_today = { 'id' => 'USN-4711-1', 'published' => '2024-10-24T19:45:29.964Z' }
allow(response).to receive(:body).and_return({ 'notices' => [notice_today, *notices.slice(0, 4)] })
allow(Date).to receive(:today).and_return(Date.new(2024, 10, 24))

expect(usa.yesterdays_erratum).to eq(notice)
end

it "no yesterday's erratum" do
allow(Date).to receive(:today).and_return(Date.new(2024, 10, 22))
allow(response).to receive(:body).and_return({ 'notices' => notices })

expect { usa.yesterdays_erratum }.to raise_error(RuntimeError)
end

# rubocop:disable RSpec/ExampleLength
it 'retries on server-error 5XX' do
raise_server_error = 2

allow(conn).to receive(:get).thrice do
if raise_server_error.zero?
response
else
raise_server_error -= 1
raise Faraday::ServerError
end
end

expect(usa.latest_errata).to eq(notices)
end
# rubocop:enable RSpec/ExampleLength

it 'fails on client-error 4XX' do
allow(response).to receive(:body).and_return({ 'notices' => [] })
allow(conn).to receive(:get).and_raise(Faraday::ClientError).once

expect { usa.latest_errata }.to raise_error(Faraday::ClientError)
end
end
end

0 comments on commit 031939a

Please sign in to comment.