concourse-dsa-resource/internal/resource/dsa/dsa.go
Jan Dittberner 182f21944b Use correct long description from DSA long RDF
- use date and DSA number as version identifier
- add tests for Check and Get calls
2023-01-22 13:18:57 +01:00

280 lines
5.9 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)
}
if len(input.Version.Date) > 0 {
_, 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)
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}
}