-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add connector for Ubuntu Security Api
- Loading branch information
Showing
4 changed files
with
176 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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 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 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,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 |
This file contains 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,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 |