288 lines
6.2 KiB
Go
288 lines
6.2 KiB
Go
/*
|
|
concourse-debian-dsa a Concourse CI resource type to get Debian security update information
|
|
|
|
Copyright Jan Dittberner
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package dsa
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const rdfURL = "https://www.debian.org/security/dsa"
|
|
const rdfLongURL = "https://www.debian.org/security/dsa-long"
|
|
const isoDateFormat = "2006-01-02"
|
|
|
|
var ErrNotSupported = errors.New("unsupported operation")
|
|
|
|
func parseISODate(dateStr string) (time.Time, error) {
|
|
date, err := time.Parse(isoDateFormat, dateStr)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("could not parse date: %w", err)
|
|
}
|
|
|
|
return date, nil
|
|
}
|
|
|
|
func getRdfData(full bool) (*rdfData, error) {
|
|
client := &http.Client{}
|
|
|
|
var url string
|
|
|
|
if full {
|
|
url = rdfLongURL
|
|
} else {
|
|
url = rdfURL
|
|
}
|
|
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not fetch Debian Security Announcement RDF: %w", err)
|
|
}
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
rdfBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not read RDF data from response: %w", err)
|
|
}
|
|
|
|
data := &rdfData{}
|
|
|
|
err = xml.Unmarshal(rdfBytes, data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse RDF data: %w", err)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
type Version struct {
|
|
Date string `json:"date"`
|
|
DSANumber string `json:"dsa"`
|
|
}
|
|
|
|
func (v Version) String() string {
|
|
data, _ := json.Marshal(v)
|
|
|
|
return string(data)
|
|
}
|
|
|
|
type checkInput struct {
|
|
Source any `json:"source"`
|
|
Version Version `json:"version"`
|
|
}
|
|
|
|
type getInput struct {
|
|
Source any `json:"source"`
|
|
Version Version `json:"version"`
|
|
Params any `json:"params"`
|
|
}
|
|
|
|
type MetaData struct {
|
|
Link string `json:"link"`
|
|
Package string `json:"package"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type getOutput struct {
|
|
Version Version `json:"version"`
|
|
Metadata MetaData `json:"metadata"`
|
|
}
|
|
|
|
type Resource struct {
|
|
in io.Reader
|
|
out io.Writer
|
|
}
|
|
|
|
type VersionRange struct {
|
|
versions []Version
|
|
}
|
|
|
|
func (r *VersionRange) Len() int {
|
|
return len(r.versions)
|
|
}
|
|
|
|
func (r *VersionRange) Less(i, j int) bool {
|
|
return r.versions[i].Date <= r.versions[j].Date && r.versions[i].DSANumber < r.versions[j].DSANumber
|
|
}
|
|
|
|
func (r *VersionRange) Swap(i, j int) {
|
|
tmpVer := r.versions[i]
|
|
|
|
r.versions[i] = r.versions[j]
|
|
r.versions[j] = tmpVer
|
|
}
|
|
|
|
func (r *VersionRange) addVersion(version Version) {
|
|
r.versions = append(r.versions, version)
|
|
}
|
|
|
|
func (r *Resource) Check() error {
|
|
input, err := r.readCheckInput()
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse input from Concourse: %w", err)
|
|
}
|
|
|
|
var inputDate time.Time
|
|
|
|
if len(input.Version.Date) > 0 {
|
|
inputDate, err = parseISODate(input.Version.Date)
|
|
if err != nil {
|
|
return fmt.Errorf("could not interpret version from Concourse as date: %w", err)
|
|
}
|
|
}
|
|
|
|
dsaData, err := getRdfData(false)
|
|
if err != nil {
|
|
log.Fatalf("could not get DSA RDF data: %v", err)
|
|
}
|
|
|
|
dates := &VersionRange{}
|
|
|
|
for _, item := range dsaData.Items {
|
|
parts := strings.SplitN(item.Title, " ", 2)
|
|
|
|
versionDate, err := parseISODate(item.Date)
|
|
if err != nil {
|
|
return fmt.Errorf("could not interpret date from RDF feed as date: %w", err)
|
|
}
|
|
|
|
if inputDate.IsZero() || inputDate.Before(versionDate) {
|
|
dates.addVersion(Version{Date: item.Date, DSANumber: parts[0]})
|
|
}
|
|
}
|
|
|
|
sort.Sort(dates)
|
|
|
|
err = r.writeCheckOutput(dates)
|
|
if err != nil {
|
|
return fmt.Errorf("could not write output for Concourse: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Resource) Get(_ string) error {
|
|
input, err := r.readGetInput()
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse input from Concourse: %w", err)
|
|
}
|
|
|
|
_, err = parseISODate(input.Version.Date)
|
|
if err != nil {
|
|
return fmt.Errorf("could not interpret version from Concourse as date: %w", err)
|
|
}
|
|
|
|
dsaData, err := getRdfData(true)
|
|
if err != nil {
|
|
log.Fatalf("could not get DSA RDF data: %v", err)
|
|
}
|
|
|
|
for _, item := range dsaData.Items {
|
|
if item.Date == input.Version.Date && strings.HasPrefix(item.Title, input.Version.DSANumber) {
|
|
parts := strings.SplitN(item.Title, " ", 3)
|
|
|
|
err = r.writeGetOutput(Version{
|
|
Date: item.Date,
|
|
DSANumber: parts[0],
|
|
}, MetaData{
|
|
Link: item.Link,
|
|
Package: parts[1],
|
|
Description: strings.TrimSpace(
|
|
strings.ReplaceAll(strings.ReplaceAll(
|
|
item.Description, "<p>", ""), "</p>", "\n",
|
|
),
|
|
),
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("could not write output for Concourse: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("requested version %s not found", input.Version)
|
|
}
|
|
|
|
func (r *Resource) readCheckInput() (*checkInput, error) {
|
|
dec := json.NewDecoder(r.in)
|
|
|
|
inp := &checkInput{}
|
|
|
|
err := dec.Decode(inp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not decode Concourse input: %w", err)
|
|
}
|
|
|
|
return inp, nil
|
|
}
|
|
|
|
func (r *Resource) writeCheckOutput(versions *VersionRange) error {
|
|
enc := json.NewEncoder(r.out)
|
|
|
|
err := enc.Encode(versions.versions)
|
|
if err != nil {
|
|
return fmt.Errorf("could not marshal JSON: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Resource) readGetInput() (*getInput, error) {
|
|
dec := json.NewDecoder(r.in)
|
|
|
|
inp := &getInput{}
|
|
|
|
err := dec.Decode(inp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not decode Concourse input: %w", err)
|
|
}
|
|
|
|
return inp, nil
|
|
}
|
|
|
|
func (r *Resource) writeGetOutput(version Version, metaData MetaData) error {
|
|
enc := json.NewEncoder(r.out)
|
|
|
|
output := &getOutput{
|
|
Version: version,
|
|
Metadata: metaData,
|
|
}
|
|
|
|
err := enc.Encode(&output)
|
|
if err != nil {
|
|
return fmt.Errorf("could not marshal JSON: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func NewResource(in io.Reader, out io.Writer) *Resource {
|
|
return &Resource{in: in, out: out}
|
|
}
|