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