Initial release
This commit is contained in:
commit
12b2d36004
10 changed files with 1176 additions and 0 deletions
269
internal/resource/dsa/dsa.go
Normal file
269
internal/resource/dsa/dsa.go
Normal file
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
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"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
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"`
|
||||
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].Title < r.versions[j].Title
|
||||
}
|
||||
|
||||
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 {
|
||||
dates.addVersion(Version{Date: item.Date, Title: item.Title})
|
||||
}
|
||||
|
||||
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(false)
|
||||
if err != nil {
|
||||
log.Fatalf("could not get DSA RDF data: %v", err)
|
||||
}
|
||||
|
||||
for _, item := range dsaData.Items {
|
||||
if item.Date == input.Version.Date && item.Title == input.Version.Title {
|
||||
err = r.writeGetOutput(Version{
|
||||
Date: item.Date,
|
||||
Title: item.Title,
|
||||
}, MetaData{
|
||||
Link: item.Link,
|
||||
Description: strings.TrimSpace(item.Description),
|
||||
})
|
||||
|
||||
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}
|
||||
}
|
62
internal/resource/dsa/dsardf.go
Normal file
62
internal/resource/dsa/dsardf.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
concourse-dsa-resource 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 (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type rdfItem struct {
|
||||
Resource string `xml:"resource,attr"`
|
||||
}
|
||||
|
||||
type rdfSeq struct {
|
||||
Items []rdfItem `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# li"`
|
||||
}
|
||||
|
||||
type rdfItems struct {
|
||||
RdfSeq rdfSeq `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# Seq"`
|
||||
}
|
||||
|
||||
type rssChannel struct {
|
||||
Title string `xml:"http://purl.org/rss/1.0/ title"`
|
||||
Items rdfItems `xml:"http://purl.org/rss/1.0/ items"`
|
||||
}
|
||||
|
||||
type rssItem struct {
|
||||
Title string `xml:"http://purl.org/rss/1.0/ title"`
|
||||
Link string `xml:"http://purl.org/rss/1.0/ link"`
|
||||
Description string `xml:"http://purl.org/rss/1.0/ description,omitempty"`
|
||||
Date string `xml:"http://purl.org/dc/elements/1.1/ date"`
|
||||
}
|
||||
|
||||
func (i rssItem) String() string {
|
||||
return fmt.Sprintf(
|
||||
"Item[Title: %s\nLink: %s\nDate: %s]",
|
||||
i.Title,
|
||||
i.Link,
|
||||
i.Date,
|
||||
)
|
||||
}
|
||||
|
||||
type rdfData struct {
|
||||
Channel rssChannel `xml:"http://purl.org/rss/1.0/ channel"`
|
||||
Items []rssItem `xml:"http://purl.org/rss/1.0/ item"`
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue