mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 18:15:42 +01:00
Merge branch 'main' into client-certificate
This commit is contained in:
commit
3753f08370
18 changed files with 537 additions and 168 deletions
3
.github/workflows/go.yml
vendored
3
.github/workflows/go.yml
vendored
|
|
@ -29,3 +29,6 @@ jobs:
|
||||||
|
|
||||||
- name: golint
|
- name: golint
|
||||||
uses: Jerome1337/golint-action@v1.0.2
|
uses: Jerome1337/golint-action@v1.0.2
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: go test -v ./...
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -2,15 +2,13 @@
|
||||||
|
|
||||||
**WIP**: A proof of concept for a CSAF trusted provider, checker and aggregator.
|
**WIP**: A proof of concept for a CSAF trusted provider, checker and aggregator.
|
||||||
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
- A recent version of **Go** (1.17+) should be installed. [Go installation](https://go.dev/doc/install)
|
- A recent version of **Go** (1.17+) should be installed. [Go installation](https://go.dev/doc/install)
|
||||||
|
|
||||||
- Clone the repository `git clone https://github.com/csaf-poc/csaf_distribution.git `
|
- Clone the repository `git clone https://github.com/csaf-poc/csaf_distribution.git `
|
||||||
|
|
||||||
- Build Go components
|
- Build Go components Makefile supplies the following targets:
|
||||||
Makefile supplies the following targets:
|
|
||||||
- Build For GNU/Linux System: `make build_linux`
|
- Build For GNU/Linux System: `make build_linux`
|
||||||
- Build For Windows System (cross build): `make build_win`
|
- Build For Windows System (cross build): `make build_win`
|
||||||
- Build For both linux and windows: `make build`
|
- Build For both linux and windows: `make build`
|
||||||
|
|
@ -27,6 +25,7 @@ Binaries will be placed in directories named like `bin-linux-amd64/` and `bin-wi
|
||||||
- To configure nginx for client certificate authentication see [docs/client-certificate-setup.md](docs/client-certificate-setup.md)
|
- To configure nginx for client certificate authentication see [docs/client-certificate-setup.md](docs/client-certificate-setup.md)
|
||||||
|
|
||||||
## csaf_uploader
|
## csaf_uploader
|
||||||
|
|
||||||
csaf_uploader is a command line tool that uploads CSAF documents to the trusted provider (CSAF_Provider).
|
csaf_uploader is a command line tool that uploads CSAF documents to the trusted provider (CSAF_Provider).
|
||||||
Following options are supported:
|
Following options are supported:
|
||||||
|
|
||||||
|
|
@ -35,13 +34,14 @@ Following options are supported:
|
||||||
| -a, --action=[upload\|create] | Action to perform (default: upload) |
|
| -a, --action=[upload\|create] | Action to perform (default: upload) |
|
||||||
| -u, --url=URL | URL of the CSAF provider (default:https:<span></span>//localhost/cgi-bin/csaf_provider.go) |
|
| -u, --url=URL | URL of the CSAF provider (default:https:<span></span>//localhost/cgi-bin/csaf_provider.go) |
|
||||||
| -t, --tlp=[csaf\|white\|green\|amber\|red] | TLP of the feed (default: csaf) |
|
| -t, --tlp=[csaf\|white\|green\|amber\|red] | TLP of the feed (default: csaf) |
|
||||||
| -x, --external-signed | CASF files are signed externally. |
|
| -x, --external-signed | CSAF files are signed externally. Assumes .asc files beside CSAF files |
|
||||||
| -k, --key=KEY-FILE | OpenPGP key to sign the CSAF files |
|
| -k, --key=KEY-FILE | OpenPGP key to sign the CSAF files |
|
||||||
| -p, --password=PASSWORD | Authentication password for accessing the CSAF provider |
|
| -p, --password=PASSWORD | Authentication password for accessing the CSAF provider |
|
||||||
| -P, --passphrase=PASSPHRASE | Passphrase to unlock the OpenPGP key |
|
| -P, --passphrase=PASSPHRASE | Passphrase to unlock the OpenPGP key |
|
||||||
| -i, --password-interactive | Enter password interactively |
|
| -i, --password-interactive | Enter password interactively |
|
||||||
| -I, --passphrase-interacive | Enter passphrase interactively |
|
| -I, --passphrase-interacive | Enter passphrase interactively |
|
||||||
| -c, --config=INI-FILE | Path to config ini file |
|
| -c, --config=INI-FILE | Path to config ini file |
|
||||||
|
| --insecure | Do not check TSL certificates from provider |
|
||||||
| -h, --help | Show help |
|
| -h, --help | Show help |
|
||||||
|
|
||||||
E.g. creating the initial directiories and files
|
E.g. creating the initial directiories and files
|
||||||
|
|
@ -71,6 +71,12 @@ action=create
|
||||||
u=http://localhost/cgi-bin/csaf_provider.go
|
u=http://localhost/cgi-bin/csaf_provider.go
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## csaf_checker
|
||||||
|
|
||||||
|
Provider checker is a tool for testing a CSAF trusted provider according to [Section 7 of the CSAF standard](https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html#7-distributing-csaf-documents).
|
||||||
|
Usage example:
|
||||||
|
``` ./csaf_checker example.com -f html -o check-results.html```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
- csaf_distribution is licensed as Free Software under MIT License.
|
- csaf_distribution is licensed as Free Software under MIT License.
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ func errCheck(err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeJSON writes the JSON encoding of the given report to the given stream.
|
||||||
|
// It returns nil, otherwise an error.
|
||||||
func writeJSON(report *Report, w io.WriteCloser) error {
|
func writeJSON(report *Report, w io.WriteCloser) error {
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
|
|
@ -48,6 +50,8 @@ func writeJSON(report *Report, w io.WriteCloser) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeHTML writes the given report to the given writer, it uses the template
|
||||||
|
// in the "reportHTML" variable. It returns nil, otherwise an error.
|
||||||
func writeHTML(report *Report, w io.WriteCloser) error {
|
func writeHTML(report *Report, w io.WriteCloser) error {
|
||||||
tmpl, err := template.New("Report HTML").Parse(reportHTML)
|
tmpl, err := template.New("Report HTML").Parse(reportHTML)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -72,6 +76,8 @@ type nopCloser struct{ io.Writer }
|
||||||
|
|
||||||
func (nc *nopCloser) Close() error { return nil }
|
func (nc *nopCloser) Close() error { return nil }
|
||||||
|
|
||||||
|
// writeReport defines where to write the report according to the "output" flag option.
|
||||||
|
// It calls also the "writeJSON" or "writeHTML" function according to the "format" flag option.
|
||||||
func writeReport(report *Report, opts *options) error {
|
func writeReport(report *Report, opts *options) error {
|
||||||
|
|
||||||
var w io.WriteCloser
|
var w io.WriteCloser
|
||||||
|
|
@ -98,6 +104,8 @@ func writeReport(report *Report, opts *options) error {
|
||||||
return writer(report, w)
|
return writer(report, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildReporters initializes each report by assigning a number and description to it.
|
||||||
|
// It returns an array of the reporter interface type.
|
||||||
func buildReporters() []reporter {
|
func buildReporters() []reporter {
|
||||||
return []reporter{
|
return []reporter{
|
||||||
&tlsReporter{baseReporter{num: 3, description: "TLS"}},
|
&tlsReporter{baseReporter{num: 3, description: "TLS"}},
|
||||||
|
|
@ -112,7 +120,7 @@ func buildReporters() []reporter {
|
||||||
&directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}},
|
&directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}},
|
||||||
&integrityReporter{baseReporter{num: 18, description: "Integrity"}},
|
&integrityReporter{baseReporter{num: 18, description: "Integrity"}},
|
||||||
&signaturesReporter{baseReporter{num: 19, description: "Signatures"}},
|
&signaturesReporter{baseReporter{num: 19, description: "Signatures"}},
|
||||||
&publicPGPKeyReporter{baseReporter{num: 20, description: "Public PGP Key"}},
|
&publicPGPKeyReporter{baseReporter{num: 20, description: "Public OpenPGP Key"}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
|
@ -20,6 +19,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
@ -28,8 +28,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/PaesslerAG/gval"
|
|
||||||
"github.com/PaesslerAG/jsonpath"
|
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
|
|
||||||
"github.com/csaf-poc/csaf_distribution/csaf"
|
"github.com/csaf-poc/csaf_distribution/csaf"
|
||||||
|
|
@ -43,6 +41,7 @@ type processor struct {
|
||||||
redirects map[string]string
|
redirects map[string]string
|
||||||
noneTLS map[string]struct{}
|
noneTLS map[string]struct{}
|
||||||
alreadyChecked map[string]whereType
|
alreadyChecked map[string]whereType
|
||||||
|
pmdURL string
|
||||||
pmd256 []byte
|
pmd256 []byte
|
||||||
pmd interface{}
|
pmd interface{}
|
||||||
keys []*crypto.KeyRing
|
keys []*crypto.KeyRing
|
||||||
|
|
@ -56,10 +55,12 @@ type processor struct {
|
||||||
badChanges []string
|
badChanges []string
|
||||||
badFolders []string
|
badFolders []string
|
||||||
|
|
||||||
builder gval.Language
|
expr *util.PathEval
|
||||||
exprs map[string]gval.Evaluable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reporter is implemented by any value that has a report method.
|
||||||
|
// The implementation of the report controls how to test
|
||||||
|
// the respective requirement and generate the report.
|
||||||
type reporter interface {
|
type reporter interface {
|
||||||
report(*processor, *Domain)
|
report(*processor, *Domain)
|
||||||
}
|
}
|
||||||
|
|
@ -104,21 +105,24 @@ func (wt whereType) String() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newProcessor returns a processor structure after assigning the given options to the opts attribute
|
||||||
|
// and initializing the "alreadyChecked" and "expr" fields.
|
||||||
func newProcessor(opts *options) *processor {
|
func newProcessor(opts *options) *processor {
|
||||||
return &processor{
|
return &processor{
|
||||||
opts: opts,
|
opts: opts,
|
||||||
alreadyChecked: map[string]whereType{},
|
alreadyChecked: map[string]whereType{},
|
||||||
builder: gval.Full(jsonpath.Language()),
|
expr: util.NewPathEval(),
|
||||||
exprs: map[string]gval.Evaluable{},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clean clears the fields values of the given processor.
|
||||||
func (p *processor) clean() {
|
func (p *processor) clean() {
|
||||||
p.redirects = nil
|
p.redirects = nil
|
||||||
p.noneTLS = nil
|
p.noneTLS = nil
|
||||||
for k := range p.alreadyChecked {
|
for k := range p.alreadyChecked {
|
||||||
delete(p.alreadyChecked, k)
|
delete(p.alreadyChecked, k)
|
||||||
}
|
}
|
||||||
|
p.pmdURL = ""
|
||||||
p.pmd256 = nil
|
p.pmd256 = nil
|
||||||
p.pmd = nil
|
p.pmd = nil
|
||||||
p.keys = nil
|
p.keys = nil
|
||||||
|
|
@ -131,6 +135,10 @@ func (p *processor) clean() {
|
||||||
p.badIndices = nil
|
p.badIndices = nil
|
||||||
p.badChanges = nil
|
p.badChanges = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// run calls checkDomain function for each domain in the given "domains" parameter.
|
||||||
|
// Then it calls the report method on each report from the given "reporters" paramerter for each domain.
|
||||||
|
// It returns a pointer to the report and nil, otherwise an error.
|
||||||
func (p *processor) run(reporters []reporter, domains []string) (*Report, error) {
|
func (p *processor) run(reporters []reporter, domains []string) (*Report, error) {
|
||||||
|
|
||||||
var report Report
|
var report Report
|
||||||
|
|
@ -174,21 +182,8 @@ func (p *processor) checkDomain(domain string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) jsonPath(expr string, doc interface{}) (interface{}, error) {
|
// checkTLS parses the given URL to check its schema, as a result it sets
|
||||||
if doc == nil {
|
// the value of "noneTLS" field if it is not HTTPS.
|
||||||
return nil, errors.New("no document to extract data from")
|
|
||||||
}
|
|
||||||
eval := p.exprs[expr]
|
|
||||||
if eval == nil {
|
|
||||||
var err error
|
|
||||||
if eval, err = p.builder.NewEvaluable(expr); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.exprs[expr] = eval
|
|
||||||
}
|
|
||||||
return eval(context.Background(), doc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *processor) checkTLS(u string) {
|
func (p *processor) checkTLS(u string) {
|
||||||
if p.noneTLS == nil {
|
if p.noneTLS == nil {
|
||||||
p.noneTLS = map[string]struct{}{}
|
p.noneTLS = map[string]struct{}{}
|
||||||
|
|
@ -247,6 +242,7 @@ func (p *processor) httpClient() *http.Client {
|
||||||
return p.client
|
return p.client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// use checks the given array and initializes an empty array if its nil.
|
||||||
func use(s *[]string) {
|
func use(s *[]string) {
|
||||||
if *s == nil {
|
if *s == nil {
|
||||||
*s = []string{}
|
*s = []string{}
|
||||||
|
|
@ -257,34 +253,50 @@ func used(s []string) bool {
|
||||||
return s != nil
|
return s != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// badIntegrity appends a message to the value of "badIntegrity" field of
|
||||||
|
// the "processor" struct according to the given format and parameters.
|
||||||
func (p *processor) badIntegrity(format string, args ...interface{}) {
|
func (p *processor) badIntegrity(format string, args ...interface{}) {
|
||||||
p.badIntegrities = append(p.badIntegrities, fmt.Sprintf(format, args...))
|
p.badIntegrities = append(p.badIntegrities, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// badSignature appends a message to the value of "badSignature" field of
|
||||||
|
// the "processor" struct according to the given format and parameters.
|
||||||
func (p *processor) badSignature(format string, args ...interface{}) {
|
func (p *processor) badSignature(format string, args ...interface{}) {
|
||||||
p.badSignatures = append(p.badSignatures, fmt.Sprintf(format, args...))
|
p.badSignatures = append(p.badSignatures, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// badProviderMetadata appends a message to the value of "badProviderMetadatas" field of
|
||||||
|
// the "processor" struct according to the given format and parameters.
|
||||||
func (p *processor) badProviderMetadata(format string, args ...interface{}) {
|
func (p *processor) badProviderMetadata(format string, args ...interface{}) {
|
||||||
p.badProviderMetadatas = append(p.badProviderMetadatas, fmt.Sprintf(format, args...))
|
p.badProviderMetadatas = append(p.badProviderMetadatas, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// badPGP appends a message to the value of "badPGPs" field of
|
||||||
|
// the "processor" struct according to the given format and parameters.
|
||||||
func (p *processor) badPGP(format string, args ...interface{}) {
|
func (p *processor) badPGP(format string, args ...interface{}) {
|
||||||
p.badPGPs = append(p.badPGPs, fmt.Sprintf(format, args...))
|
p.badPGPs = append(p.badPGPs, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// badSecurity appends a message to the value of "badSecurity" field of
|
||||||
|
// the "processor" struct according to the given format and parameters.
|
||||||
func (p *processor) badSecurity(format string, args ...interface{}) {
|
func (p *processor) badSecurity(format string, args ...interface{}) {
|
||||||
p.badSecurities = append(p.badSecurities, fmt.Sprintf(format, args...))
|
p.badSecurities = append(p.badSecurities, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// badIndex appends a message to the value of "badIndices" field of
|
||||||
|
// the "processor" struct according to the given format and parameters.
|
||||||
func (p *processor) badIndex(format string, args ...interface{}) {
|
func (p *processor) badIndex(format string, args ...interface{}) {
|
||||||
p.badIndices = append(p.badIndices, fmt.Sprintf(format, args...))
|
p.badIndices = append(p.badIndices, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// badChange appends a message to the value of "badChanges" field of
|
||||||
|
// the "processor" struct according to the given format and parameters.
|
||||||
func (p *processor) badChange(format string, args ...interface{}) {
|
func (p *processor) badChange(format string, args ...interface{}) {
|
||||||
p.badChanges = append(p.badChanges, fmt.Sprintf(format, args...))
|
p.badChanges = append(p.badChanges, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// badFolder appends a message to the value of "badFolders" field of
|
||||||
|
// the "processor" struct according to the given format and parameters.
|
||||||
func (p *processor) badFolder(format string, args ...interface{}) {
|
func (p *processor) badFolder(format string, args ...interface{}) {
|
||||||
p.badFolders = append(p.badFolders, fmt.Sprintf(format, args...))
|
p.badFolders = append(p.badFolders, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +367,7 @@ func (p *processor) integrity(
|
||||||
// Check if file is in the right folder.
|
// Check if file is in the right folder.
|
||||||
use(&p.badFolders)
|
use(&p.badFolders)
|
||||||
|
|
||||||
if date, err := p.jsonPath(
|
if date, err := p.expr.Eval(
|
||||||
`$.document.tracking.initial_release_date`, doc); err != nil {
|
`$.document.tracking.initial_release_date`, doc); err != nil {
|
||||||
p.badFolder(
|
p.badFolder(
|
||||||
"Extracting 'initial_release_date' from %s failed: %v", u, err)
|
"Extracting 'initial_release_date' from %s failed: %v", u, err)
|
||||||
|
|
@ -486,7 +498,7 @@ func (p *processor) processROLIEFeed(feed string) error {
|
||||||
|
|
||||||
// Extract the CSAF files from feed.
|
// Extract the CSAF files from feed.
|
||||||
var files []string
|
var files []string
|
||||||
for _, f := range rfeed.Entry {
|
for _, f := range rfeed.Feed.Entry {
|
||||||
for i := range f.Link {
|
for i := range f.Link {
|
||||||
files = append(files, f.Link[i].HRef)
|
files = append(files, f.Link[i].HRef)
|
||||||
}
|
}
|
||||||
|
|
@ -508,6 +520,9 @@ func (p *processor) processROLIEFeed(feed string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkIndex fetches the "index.txt" and calls "checkTLS" method for HTTPS checks.
|
||||||
|
// It extracts the file names from the file and passes them to "integrity" function.
|
||||||
|
// It returns error if fetching/reading the file(s) fails, otherwise nil.
|
||||||
func (p *processor) checkIndex(base string, mask whereType) error {
|
func (p *processor) checkIndex(base string, mask whereType) error {
|
||||||
client := p.httpClient()
|
client := p.httpClient()
|
||||||
index := base + "/index.txt"
|
index := base + "/index.txt"
|
||||||
|
|
@ -546,6 +561,10 @@ func (p *processor) checkIndex(base string, mask whereType) error {
|
||||||
return p.integrity(files, base, mask, p.badIndex)
|
return p.integrity(files, base, mask, p.badIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkChanges fetches the "changes.csv" and calls the "checkTLS" method for HTTPs checks.
|
||||||
|
// It extracts the file content, tests the column number and the validity of the time format
|
||||||
|
// of the fields' values and if they are sorted properly. Then it passes the files to the
|
||||||
|
// "integrity" functions. It returns error if some test fails, otherwise nil.
|
||||||
func (p *processor) checkChanges(base string, mask whereType) error {
|
func (p *processor) checkChanges(base string, mask whereType) error {
|
||||||
client := p.httpClient()
|
client := p.httpClient()
|
||||||
changes := base + "/changes.csv"
|
changes := base + "/changes.csv"
|
||||||
|
|
@ -607,7 +626,7 @@ func (p *processor) checkChanges(base string, mask whereType) error {
|
||||||
|
|
||||||
func (p *processor) processROLIEFeeds(domain string, feeds [][]csaf.Feed) error {
|
func (p *processor) processROLIEFeeds(domain string, feeds [][]csaf.Feed) error {
|
||||||
|
|
||||||
base, err := url.Parse("https://" + domain + "/.well-known/csaf/")
|
base, err := url.Parse(p.pmdURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -634,7 +653,7 @@ func (p *processor) processROLIEFeeds(domain string, feeds [][]csaf.Feed) error
|
||||||
|
|
||||||
func (p *processor) checkCSAFs(domain string) error {
|
func (p *processor) checkCSAFs(domain string) error {
|
||||||
// Check for ROLIE
|
// Check for ROLIE
|
||||||
rolie, err := p.jsonPath("$.distributions[*].rolie.feeds", p.pmd)
|
rolie, err := p.expr.Eval("$.distributions[*].rolie.feeds", p.pmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -654,7 +673,10 @@ func (p *processor) checkCSAFs(domain string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// No rolie feeds
|
// No rolie feeds
|
||||||
base := "https://" + domain + "/.well-known/csaf"
|
base, err := basePath(p.pmdURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.checkIndex(base, indexMask); err != nil && err != errContinue {
|
if err := p.checkIndex(base, indexMask); err != nil && err != errContinue {
|
||||||
return err
|
return err
|
||||||
|
|
@ -701,53 +723,156 @@ func (p *processor) checkMissing(string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) checkProviderMetadata(domain string) error {
|
var providerMetadataLocations = [...]string{
|
||||||
|
".well-known/csaf",
|
||||||
|
"security/data/csaf",
|
||||||
|
"advisories/csaf",
|
||||||
|
"security/csaf",
|
||||||
|
}
|
||||||
|
|
||||||
|
// locateProviderMetadata searches for provider-metadata.json at various
|
||||||
|
// locations mentioned in "7.1.7 Requirement 7: provider-metadata.json".
|
||||||
|
func (p *processor) locateProviderMetadata(
|
||||||
|
domain string,
|
||||||
|
found func(string, io.Reader) error,
|
||||||
|
) error {
|
||||||
|
|
||||||
client := p.httpClient()
|
client := p.httpClient()
|
||||||
|
|
||||||
url := "https://" + domain + "/.well-known/csaf/provider-metadata.json"
|
tryURL := func(url string) (bool, error) {
|
||||||
|
res, err := client.Get(url)
|
||||||
|
if err != nil || res.StatusCode != http.StatusOK ||
|
||||||
|
res.Header.Get("Content-Type") != "application/json" {
|
||||||
|
// ignore this as it is expected.
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
use(&p.badProviderMetadatas)
|
if err := func() error {
|
||||||
|
defer res.Body.Close()
|
||||||
res, err := client.Get(url)
|
return found(url, res.Body)
|
||||||
if err != nil {
|
}(); err != nil {
|
||||||
p.badProviderMetadata("Fetching %s: %v.", url, err)
|
return false, err
|
||||||
return errStop
|
}
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
for _, loc := range providerMetadataLocations {
|
||||||
p.badProviderMetadata("Fetching %s failed. Status code: %d (%s)",
|
url := "https://" + domain + "/" + loc
|
||||||
url, res.StatusCode, res.Status)
|
ok, err := tryURL(url)
|
||||||
return errStop
|
if err != nil {
|
||||||
|
if err == errContinue {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate checksum for later comparison.
|
// Read from security.txt
|
||||||
hash := sha256.New()
|
|
||||||
|
|
||||||
if err := func() error {
|
path := "https://" + domain + "/.well-known/security.txt"
|
||||||
defer res.Body.Close()
|
res, err := client.Get(path)
|
||||||
tee := io.TeeReader(res.Body, hash)
|
|
||||||
return json.NewDecoder(tee).Decode(&p.pmd)
|
|
||||||
}(); err != nil {
|
|
||||||
p.badProviderMetadata("Decoding JSON failed: %v", err)
|
|
||||||
return errStop
|
|
||||||
}
|
|
||||||
|
|
||||||
p.pmd256 = hash.Sum(nil)
|
|
||||||
|
|
||||||
errors, err := csaf.ValidateProviderMetadata(p.pmd)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(errors) > 0 {
|
|
||||||
p.badProviderMetadata("Validating against JSON schema failed:")
|
if res.StatusCode != http.StatusOK {
|
||||||
for _, msg := range errors {
|
return err
|
||||||
p.badProviderMetadata(strings.ReplaceAll(msg, `%`, `%%`))
|
}
|
||||||
|
|
||||||
|
loc, err := func() (string, error) {
|
||||||
|
defer res.Body.Close()
|
||||||
|
return extractProviderURL(res.Body)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error: %v\n", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if loc != "" {
|
||||||
|
if _, err = tryURL(loc); err == errContinue {
|
||||||
|
err = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractProviderURL(r io.Reader) (string, error) {
|
||||||
|
sc := bufio.NewScanner(r)
|
||||||
|
const csaf = "CSAF:"
|
||||||
|
|
||||||
|
for sc.Scan() {
|
||||||
|
line := sc.Text()
|
||||||
|
if strings.HasPrefix(line, csaf) {
|
||||||
|
line = strings.TrimSpace(line[len(csaf):])
|
||||||
|
if !strings.HasPrefix(line, "https://") {
|
||||||
|
return "", errors.New("CSAF: found in security.txt, but does not start with https://")
|
||||||
|
}
|
||||||
|
return line, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := sc.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkProviderMetadata checks the provider-metatdata if exists, decodes,
|
||||||
|
// and validates against the JSON schema. According to the result the respective
|
||||||
|
// error messages are passed to the badProviderMetadatas method in case of errors.
|
||||||
|
// It returns nil if all checks are passed.
|
||||||
|
func (p *processor) checkProviderMetadata(domain string) error {
|
||||||
|
|
||||||
|
use(&p.badProviderMetadatas)
|
||||||
|
|
||||||
|
found := func(url string, content io.Reader) error {
|
||||||
|
|
||||||
|
// Calculate checksum for later comparison.
|
||||||
|
hash := sha256.New()
|
||||||
|
|
||||||
|
tee := io.TeeReader(content, hash)
|
||||||
|
if err := json.NewDecoder(tee).Decode(&p.pmd); err != nil {
|
||||||
|
p.badProviderMetadata("%s: Decoding JSON failed: %v", url, err)
|
||||||
|
return errContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
p.pmd256 = hash.Sum(nil)
|
||||||
|
|
||||||
|
errors, err := csaf.ValidateProviderMetadata(p.pmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(errors) > 0 {
|
||||||
|
p.badProviderMetadata("%s: Validating against JSON schema failed:", url)
|
||||||
|
for _, msg := range errors {
|
||||||
|
p.badProviderMetadata(strings.ReplaceAll(msg, `%`, `%%`))
|
||||||
|
}
|
||||||
|
return errStop
|
||||||
|
}
|
||||||
|
p.pmdURL = url
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.locateProviderMetadata(domain, found); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.pmdURL == "" {
|
||||||
|
p.badProviderMetadata("No provider-metadata.json found.")
|
||||||
|
return errStop
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkSecurity checks the security.txt file by making HTTP request to fetch it.
|
||||||
|
// It checks the existence of the CSAF field in the file content and tries to fetch
|
||||||
|
// the value of this field. As a result of these a respective error messages are
|
||||||
|
// passed to the badSecurity method in case of errors.
|
||||||
|
// It returns nil if all checks are passed.
|
||||||
func (p *processor) checkSecurity(domain string) error {
|
func (p *processor) checkSecurity(domain string) error {
|
||||||
|
|
||||||
client := p.httpClient()
|
client := p.httpClient()
|
||||||
|
|
@ -826,24 +951,28 @@ func (p *processor) checkSecurity(domain string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkPGPKeys checks if the OpenPGP keys are available and valid, fetches
|
||||||
|
// the the remotely keys and compares the fingerprints.
|
||||||
|
// As a result of these a respective error messages are passed to badPGP method
|
||||||
|
// in case of errors. It returns nil if all checks are passed.
|
||||||
func (p *processor) checkPGPKeys(domain string) error {
|
func (p *processor) checkPGPKeys(domain string) error {
|
||||||
|
|
||||||
use(&p.badPGPs)
|
use(&p.badPGPs)
|
||||||
|
|
||||||
src, err := p.jsonPath("$.pgp_keys", p.pmd)
|
src, err := p.expr.Eval("$.pgp_keys", p.pmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.badPGP("No PGP keys found: %v.", err)
|
p.badPGP("No public OpenPGP keys found: %v.", err)
|
||||||
return errContinue
|
return errContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys []csaf.PGPKey
|
var keys []csaf.PGPKey
|
||||||
if err := util.ReMarshalJSON(&keys, src); err != nil {
|
if err := util.ReMarshalJSON(&keys, src); err != nil {
|
||||||
p.badPGP("PGP keys invalid: %v.", err)
|
p.badPGP("Invalid public OpenPGP keys: %v.", err)
|
||||||
return errContinue
|
return errContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
p.badPGP("No PGP keys found.")
|
p.badPGP("No public OpenPGP keys found.")
|
||||||
return errContinue
|
return errContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -851,7 +980,7 @@ func (p *processor) checkPGPKeys(domain string) error {
|
||||||
|
|
||||||
client := p.httpClient()
|
client := p.httpClient()
|
||||||
|
|
||||||
base, err := url.Parse("https://" + domain + "/.well-known/csaf/provider-metadata.json")
|
base, err := url.Parse(p.pmdURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -873,11 +1002,11 @@ func (p *processor) checkPGPKeys(domain string) error {
|
||||||
|
|
||||||
res, err := client.Get(u)
|
res, err := client.Get(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.badPGP("Fetching PGP key %s failed: %v.", u, err)
|
p.badPGP("Fetching public OpenPGP key %s failed: %v.", u, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
p.badPGP("Fetching PGP key %s status code: %d (%s)",
|
p.badPGP("Fetching public OpenPGP key %s status code: %d (%s)",
|
||||||
u, res.StatusCode, res.Status)
|
u, res.StatusCode, res.Status)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -888,24 +1017,24 @@ func (p *processor) checkPGPKeys(domain string) error {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.badPGP("Reading PGP key %s failed: %v", u, err)
|
p.badPGP("Reading public OpenPGP key %s failed: %v", u, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ckey.GetFingerprint() != string(key.Fingerprint) {
|
if ckey.GetFingerprint() != string(key.Fingerprint) {
|
||||||
p.badPGP("Fingerprint of PGP key %s do not match remotely loaded.", u)
|
p.badPGP("Fingerprint of public OpenPGP key %s does not match remotely loaded.", u)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
keyring, err := crypto.NewKeyRing(ckey)
|
keyring, err := crypto.NewKeyRing(ckey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.badPGP("Creating key ring for %s failed: %v.", u, err)
|
p.badPGP("Creating store for public OpenPGP key %s failed: %v.", u, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p.keys = append(p.keys, keyring)
|
p.keys = append(p.keys, keyring)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(p.keys) == 0 {
|
if len(p.keys) == 0 {
|
||||||
p.badPGP("No PGP keys loaded.")
|
p.badPGP("No OpenPGP keys loaded.")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
//
|
//
|
||||||
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
||||||
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
|
// Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
@ -42,6 +42,9 @@ func (bc *baseReporter) requirement(domain *Domain) *Requirement {
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// report tests if the URLs are HTTPS and sets the "message" field value
|
||||||
|
// of the "Requirement" struct as a result of that.
|
||||||
|
// A list of non HTTPS URLs is included in the value of the "message" field.
|
||||||
func (r *tlsReporter) report(p *processor, domain *Domain) {
|
func (r *tlsReporter) report(p *processor, domain *Domain) {
|
||||||
req := r.requirement(domain)
|
req := r.requirement(domain)
|
||||||
if p.noneTLS == nil {
|
if p.noneTLS == nil {
|
||||||
|
|
@ -49,7 +52,7 @@ func (r *tlsReporter) report(p *processor, domain *Domain) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(p.noneTLS) == 0 {
|
if len(p.noneTLS) == 0 {
|
||||||
req.message("All tested URLs were https.")
|
req.message("All tested URLs were HTTPS.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,10 +63,12 @@ func (r *tlsReporter) report(p *processor, domain *Domain) {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
sort.Strings(urls)
|
sort.Strings(urls)
|
||||||
req.message("Following none https URLs were used:")
|
req.message("Following non-HTTPS URLs were used:")
|
||||||
req.message(urls...)
|
req.message(urls...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// report tests if redirects are used and sets the "message" field value
|
||||||
|
// of the "Requirement" struct as a result of that.
|
||||||
func (r *redirectsReporter) report(p *processor, domain *Domain) {
|
func (r *redirectsReporter) report(p *processor, domain *Domain) {
|
||||||
req := r.requirement(domain)
|
req := r.requirement(domain)
|
||||||
if len(p.redirects) == 0 {
|
if len(p.redirects) == 0 {
|
||||||
|
|
@ -84,6 +89,8 @@ func (r *redirectsReporter) report(p *processor, domain *Domain) {
|
||||||
req.Messages = keys
|
req.Messages = keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// report tests if an provider-metatdata.json are available and sets the
|
||||||
|
// "message" field value of the "Requirement" struct as a result of that.
|
||||||
func (r *providerMetadataReport) report(p *processor, domain *Domain) {
|
func (r *providerMetadataReport) report(p *processor, domain *Domain) {
|
||||||
req := r.requirement(domain)
|
req := r.requirement(domain)
|
||||||
if !used(p.badProviderMetadatas) {
|
if !used(p.badProviderMetadatas) {
|
||||||
|
|
@ -91,12 +98,14 @@ func (r *providerMetadataReport) report(p *processor, domain *Domain) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(p.badProviderMetadatas) == 0 {
|
if len(p.badProviderMetadatas) == 0 {
|
||||||
req.message("No problems with provider metadata.")
|
req.message("Found good provider metadata.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Messages = p.badProviderMetadatas
|
req.Messages = p.badProviderMetadatas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// report tests the "security.txt" file and sets the "message" field value
|
||||||
|
// of the "Requirement" struct as a result of that.
|
||||||
func (r *securityReporter) report(p *processor, domain *Domain) {
|
func (r *securityReporter) report(p *processor, domain *Domain) {
|
||||||
req := r.requirement(domain)
|
req := r.requirement(domain)
|
||||||
if !used(p.badSecurities) {
|
if !used(p.badSecurities) {
|
||||||
|
|
@ -104,7 +113,7 @@ func (r *securityReporter) report(p *processor, domain *Domain) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(p.badSecurities) == 0 {
|
if len(p.badSecurities) == 0 {
|
||||||
req.message("No problems with security.txt found.")
|
req.message("Found good security.txt.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Messages = p.badSecurities
|
req.Messages = p.badSecurities
|
||||||
|
|
@ -113,19 +122,19 @@ func (r *securityReporter) report(p *processor, domain *Domain) {
|
||||||
func (r *wellknownMetadataReporter) report(_ *processor, domain *Domain) {
|
func (r *wellknownMetadataReporter) report(_ *processor, domain *Domain) {
|
||||||
// TODO: Implement me!
|
// TODO: Implement me!
|
||||||
req := r.requirement(domain)
|
req := r.requirement(domain)
|
||||||
_ = req
|
req.message("(Not checked, missing implementation.)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dnsPathReporter) report(_ *processor, domain *Domain) {
|
func (r *dnsPathReporter) report(_ *processor, domain *Domain) {
|
||||||
// TODO: Implement me!
|
// TODO: Implement me!
|
||||||
req := r.requirement(domain)
|
req := r.requirement(domain)
|
||||||
_ = req
|
req.message("(Not checked, missing implementation.)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *oneFolderPerYearReport) report(p *processor, domain *Domain) {
|
func (r *oneFolderPerYearReport) report(p *processor, domain *Domain) {
|
||||||
req := r.requirement(domain)
|
req := r.requirement(domain)
|
||||||
if !used(p.badFolders) {
|
if !used(p.badFolders) {
|
||||||
req.message("No checks if file are in right folders were performed.")
|
req.message("No checks if files are in right folders were performed.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(p.badFolders) == 0 {
|
if len(p.badFolders) == 0 {
|
||||||
|
|
@ -142,7 +151,7 @@ func (r *indexReporter) report(p *processor, domain *Domain) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(p.badIndices) == 0 {
|
if len(p.badIndices) == 0 {
|
||||||
req.message("No problems with index.txt found.")
|
req.message("Found good index.txt.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Messages = p.badIndices
|
req.Messages = p.badIndices
|
||||||
|
|
@ -155,7 +164,7 @@ func (r *changesReporter) report(p *processor, domain *Domain) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(p.badChanges) == 0 {
|
if len(p.badChanges) == 0 {
|
||||||
req.message("No problems with changes.csv found.")
|
req.message("Found good changes.csv.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Messages = p.badChanges
|
req.Messages = p.badChanges
|
||||||
|
|
@ -195,11 +204,11 @@ func (r *signaturesReporter) report(p *processor, domain *Domain) {
|
||||||
func (r *publicPGPKeyReporter) report(p *processor, domain *Domain) {
|
func (r *publicPGPKeyReporter) report(p *processor, domain *Domain) {
|
||||||
req := r.requirement(domain)
|
req := r.requirement(domain)
|
||||||
if !used(p.badPGPs) {
|
if !used(p.badPGPs) {
|
||||||
req.message("No PGP keys loaded.")
|
req.message("No public OpenPGP keys loaded.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Messages = p.badPGPs
|
req.Messages = p.badPGPs
|
||||||
if len(p.keys) > 0 {
|
if len(p.keys) > 0 {
|
||||||
req.message(fmt.Sprintf("%d PGP key(s) loaded successfully.", len(p.keys)))
|
req.message(fmt.Sprintf("%d public OpenPGP key(s) loaded.", len(p.keys)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ import (
|
||||||
|
|
||||||
const dateFormat = time.RFC3339
|
const dateFormat = time.RFC3339
|
||||||
|
|
||||||
|
// cleanFileName removes the "/" "\" charachters and replace the two or more
|
||||||
|
// occurences of "." with only one from the passed string.
|
||||||
func cleanFileName(s string) string {
|
func cleanFileName(s string) string {
|
||||||
s = strings.ReplaceAll(s, `/`, ``)
|
s = strings.ReplaceAll(s, `/`, ``)
|
||||||
s = strings.ReplaceAll(s, `\`, ``)
|
s = strings.ReplaceAll(s, `\`, ``)
|
||||||
|
|
@ -37,6 +39,10 @@ func cleanFileName(s string) string {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadCSAF loads the csaf file from the request, calls the "UploadLimter" function to
|
||||||
|
// set the upload limit size of the file and the "cleanFileName" to refine
|
||||||
|
// the filename. It returns the filename, file content in a buffer of bytes
|
||||||
|
// and an error.
|
||||||
func (c *controller) loadCSAF(r *http.Request) (string, []byte, error) {
|
func (c *controller) loadCSAF(r *http.Request) (string, []byte, error) {
|
||||||
file, handler, err := r.FormFile("csaf")
|
file, handler, err := r.FormFile("csaf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -123,6 +129,8 @@ func (c *controller) tlpParam(r *http.Request) (tlp, error) {
|
||||||
return "", fmt.Errorf("unsupported TLP type '%s'", t)
|
return "", fmt.Errorf("unsupported TLP type '%s'", t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create calls the "ensureFolders" functions to create the directories and files.
|
||||||
|
// It returns a struct by success, otherwise an error.
|
||||||
func (c *controller) create(*http.Request) (interface{}, error) {
|
func (c *controller) create(*http.Request) (interface{}, error) {
|
||||||
if err := ensureFolders(c.cfg); err != nil {
|
if err := ensureFolders(c.cfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -159,7 +167,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ex, err := newExtraction(content)
|
ex, err := csaf.NewAdvisorySummary(util.NewPathEval(), content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -171,8 +179,9 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
|
||||||
|
|
||||||
// Extract real TLP from document.
|
// Extract real TLP from document.
|
||||||
if t == tlpCSAF {
|
if t == tlpCSAF {
|
||||||
if t = tlp(strings.ToLower(ex.tlpLabel)); !t.valid() || t == tlpCSAF {
|
if t = tlp(strings.ToLower(ex.TLPLabel)); !t.valid() || t == tlpCSAF {
|
||||||
return nil, fmt.Errorf("not a valid TL: %s", ex.tlpLabel)
|
return nil, fmt.Errorf(
|
||||||
|
"valid TLP label missing in document (found '%s')", t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,31 +226,33 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
|
||||||
// Create new if does not exists.
|
// Create new if does not exists.
|
||||||
if rolie == nil {
|
if rolie == nil {
|
||||||
rolie = &csaf.ROLIEFeed{
|
rolie = &csaf.ROLIEFeed{
|
||||||
ID: "csaf-feed-tlp-" + ts,
|
Feed: csaf.FeedData{
|
||||||
Title: "CSAF feed (TLP:" + string(tlpLabel) + ")",
|
ID: "csaf-feed-tlp-" + ts,
|
||||||
Link: []csaf.Link{{
|
Title: "CSAF feed (TLP:" + string(tlpLabel) + ")",
|
||||||
Rel: "rel",
|
Link: []csaf.Link{{
|
||||||
HRef: string(feedURL),
|
Rel: "rel",
|
||||||
}},
|
HRef: string(feedURL),
|
||||||
|
}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rolie.Updated = csaf.TimeStamp(time.Now())
|
rolie.Feed.Updated = csaf.TimeStamp(time.Now())
|
||||||
|
|
||||||
year := strconv.Itoa(ex.initialReleaseDate.Year())
|
year := strconv.Itoa(ex.InitialReleaseDate.Year())
|
||||||
|
|
||||||
csafURL := c.cfg.Domain +
|
csafURL := c.cfg.Domain +
|
||||||
"/.well-known/csaf/" + ts + "/" + year + "/" + newCSAF
|
"/.well-known/csaf/" + ts + "/" + year + "/" + newCSAF
|
||||||
|
|
||||||
e := rolie.EntryByID(ex.id)
|
e := rolie.EntryByID(ex.ID)
|
||||||
if e == nil {
|
if e == nil {
|
||||||
e = &csaf.Entry{ID: ex.id}
|
e = &csaf.Entry{ID: ex.ID}
|
||||||
rolie.Entry = append(rolie.Entry, e)
|
rolie.Feed.Entry = append(rolie.Feed.Entry, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Titel = ex.title
|
e.Titel = ex.Title
|
||||||
e.Published = csaf.TimeStamp(ex.initialReleaseDate)
|
e.Published = csaf.TimeStamp(ex.InitialReleaseDate)
|
||||||
e.Updated = csaf.TimeStamp(ex.currentReleaseDate)
|
e.Updated = csaf.TimeStamp(ex.CurrentReleaseDate)
|
||||||
e.Link = []csaf.Link{{
|
e.Link = []csaf.Link{{
|
||||||
Rel: "self",
|
Rel: "self",
|
||||||
HRef: csafURL,
|
HRef: csafURL,
|
||||||
|
|
@ -254,8 +265,8 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
|
||||||
Type: "application/json",
|
Type: "application/json",
|
||||||
Src: csafURL,
|
Src: csafURL,
|
||||||
}
|
}
|
||||||
if ex.summary != "" {
|
if ex.Summary != "" {
|
||||||
e.Summary = &csaf.Summary{Content: ex.summary}
|
e.Summary = &csaf.Summary{Content: ex.Summary}
|
||||||
} else {
|
} else {
|
||||||
e.Summary = nil
|
e.Summary = nil
|
||||||
}
|
}
|
||||||
|
|
@ -291,7 +302,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
|
||||||
|
|
||||||
if err := updateIndices(
|
if err := updateIndices(
|
||||||
folder, filepath.Join(year, newCSAF),
|
folder, filepath.Join(year, newCSAF),
|
||||||
ex.currentReleaseDate,
|
ex.CurrentReleaseDate,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -302,9 +313,9 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
|
||||||
warn("Publisher in provider metadata is not initialized. Forgot to configure?")
|
warn("Publisher in provider metadata is not initialized. Forgot to configure?")
|
||||||
if c.cfg.DynamicProviderMetaData {
|
if c.cfg.DynamicProviderMetaData {
|
||||||
warn("Taking publisher from CSAF")
|
warn("Taking publisher from CSAF")
|
||||||
pmd.Publisher = ex.publisher
|
pmd.Publisher = ex.Publisher
|
||||||
}
|
}
|
||||||
case !pmd.Publisher.Equals(ex.publisher):
|
case !pmd.Publisher.Equals(ex.Publisher):
|
||||||
warn("Publishers in provider metadata and CSAF do not match.")
|
warn("Publishers in provider metadata and CSAF do not match.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,7 +334,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
|
||||||
Error error `json:"-"`
|
Error error `json:"-"`
|
||||||
}{
|
}{
|
||||||
Name: newCSAF,
|
Name: newCSAF,
|
||||||
ReleaseDate: ex.currentReleaseDate.Format(dateFormat),
|
ReleaseDate: ex.CurrentReleaseDate.Format(dateFormat),
|
||||||
Warnings: warnings,
|
Warnings: warnings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// The environment name, that contains the path to the config file.
|
||||||
configEnv = "CSAF_CONFIG"
|
configEnv = "CSAF_CONFIG"
|
||||||
defaultConfigPath = "/usr/lib/casf/config.toml"
|
defaultConfigPath = "/usr/lib/csaf/config.toml" // Default path to the config file.
|
||||||
defaultFolder = "/var/www/"
|
defaultFolder = "/var/www/" // Default folder path.
|
||||||
defaultWeb = "/var/www/html"
|
defaultWeb = "/var/www/html" // Default web path.
|
||||||
defaultOpenPGPURL = "https://openpgp.circl.lu/pks/lookup?op=get&search=${FINGERPRINT}"
|
defaultOpenPGPURL = "https://openpgp.circl.lu/pks/lookup?op=get&search=${FINGERPRINT}" // Default OpenPGP URL.
|
||||||
defaultUploadLimit = 50 * 1024 * 1024
|
defaultUploadLimit = 50 * 1024 * 1024 // Default limit size of the uploaded file.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// configs contains the config values for the provider.
|
||||||
type config struct {
|
type config struct {
|
||||||
Password *string `toml:"password"`
|
Password *string `toml:"password"`
|
||||||
Key string `toml:"key"`
|
Key string `toml:"key"`
|
||||||
|
|
@ -57,6 +59,7 @@ const (
|
||||||
tlpRed tlp = "red"
|
tlpRed tlp = "red"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// valid returns true if the checked tlp matches one of the defined tlps.
|
||||||
func (t tlp) valid() bool {
|
func (t tlp) valid() bool {
|
||||||
switch t {
|
switch t {
|
||||||
case tlpCSAF, tlpWhite, tlpGreen, tlpAmber, tlpRed:
|
case tlpCSAF, tlpWhite, tlpGreen, tlpAmber, tlpRed:
|
||||||
|
|
@ -74,6 +77,8 @@ func (t *tlp) UnmarshalText(text []byte) error {
|
||||||
return fmt.Errorf("invalid config TLP value: %v", string(text))
|
return fmt.Errorf("invalid config TLP value: %v", string(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uploadLimiter returns a reader that reads from a given r reader but stops
|
||||||
|
// with EOF after the defined bytes in the "UploadLimit" config option.
|
||||||
func (cfg *config) uploadLimiter(r io.Reader) io.Reader {
|
func (cfg *config) uploadLimiter(r io.Reader) io.Reader {
|
||||||
// Zero or less means no upload limit.
|
// Zero or less means no upload limit.
|
||||||
if cfg.UploadLimit == nil || *cfg.UploadLimit < 1 {
|
if cfg.UploadLimit == nil || *cfg.UploadLimit < 1 {
|
||||||
|
|
@ -101,6 +106,8 @@ func (cfg *config) modelTLPs() []csaf.TLPLabel {
|
||||||
return tlps
|
return tlps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadCryptoKey loads the armored data into the key stored in the file specified by the
|
||||||
|
// "key" config value and return it with nil, otherwise an error.
|
||||||
func (cfg *config) loadCryptoKey() (*crypto.Key, error) {
|
func (cfg *config) loadCryptoKey() (*crypto.Key, error) {
|
||||||
f, err := os.Open(cfg.Key)
|
f, err := os.Open(cfg.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -110,11 +117,18 @@ func (cfg *config) loadCryptoKey() (*crypto.Key, error) {
|
||||||
return crypto.NewKeyFromArmoredReader(f)
|
return crypto.NewKeyFromArmoredReader(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkPassword compares the given hashed password with the plaintext in the "password" config value.
|
||||||
|
// It returns true if these matches or if the "password" config value is not set, otherwise false.
|
||||||
func (cfg *config) checkPassword(hash string) bool {
|
func (cfg *config) checkPassword(hash string) bool {
|
||||||
return cfg.Password == nil ||
|
return cfg.Password == nil ||
|
||||||
bcrypt.CompareHashAndPassword([]byte(hash), []byte(*cfg.Password)) == nil
|
bcrypt.CompareHashAndPassword([]byte(hash), []byte(*cfg.Password)) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadConfig extracts the config values from the config file. The path to the
|
||||||
|
// file is taken either from environment variable "CSAF_CONFIG" or from the
|
||||||
|
// defined default path in "defaultConfigPath".
|
||||||
|
// Default values are set in case some are missing in the file.
|
||||||
|
// It returns these values in a struct and nil if there is no error.
|
||||||
func loadConfig() (*config, error) {
|
func loadConfig() (*config, error) {
|
||||||
path := os.Getenv(configEnv)
|
path := os.Getenv(configEnv)
|
||||||
if path == "" {
|
if path == "" {
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,14 @@ func asMultiError(err error) multiError {
|
||||||
return multiError([]string{err.Error()})
|
return multiError([]string{err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// controller contains the config values and the html templates.
|
||||||
type controller struct {
|
type controller struct {
|
||||||
cfg *config
|
cfg *config
|
||||||
tmpl *template.Template
|
tmpl *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newController assigns the given configs to a controller variable and parses the html template
|
||||||
|
// if the config value "NoWebUI" is true. It returns the controller variable and nil, otherwise error.
|
||||||
func newController(cfg *config) (*controller, error) {
|
func newController(cfg *config) (*controller, error) {
|
||||||
|
|
||||||
c := controller{cfg: cfg}
|
c := controller{cfg: cfg}
|
||||||
|
|
@ -57,6 +60,8 @@ func newController(cfg *config) (*controller, error) {
|
||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bind binds the paths with the corresponding http.handler and wraps it with the respective middleware,
|
||||||
|
// according to the "NoWebUI" config value.
|
||||||
func (c *controller) bind(pim *pathInfoMux) {
|
func (c *controller) bind(pim *pathInfoMux) {
|
||||||
if !c.cfg.NoWebUI {
|
if !c.cfg.NoWebUI {
|
||||||
pim.handleFunc("/", c.auth(c.index))
|
pim.handleFunc("/", c.auth(c.index))
|
||||||
|
|
@ -67,6 +72,9 @@ func (c *controller) bind(pim *pathInfoMux) {
|
||||||
pim.handleFunc("/api/create", c.auth(api(c.create)))
|
pim.handleFunc("/api/create", c.auth(api(c.create)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// auth wraps the given http.HandlerFunc and returns an new one after authenticating the
|
||||||
|
// password contained in the header "X-CSAF-PROVIDER-AUTH" with the "password" config value
|
||||||
|
// if set, otherwise returns the given http.HandlerFunc.
|
||||||
func (c *controller) auth(
|
func (c *controller) auth(
|
||||||
fn func(http.ResponseWriter, *http.Request),
|
fn func(http.ResponseWriter, *http.Request),
|
||||||
) func(http.ResponseWriter, *http.Request) {
|
) func(http.ResponseWriter, *http.Request) {
|
||||||
|
|
@ -93,6 +101,9 @@ func (c *controller) auth(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// render sets the headers for the response. It applies the given template "tmpl" to
|
||||||
|
// the given object "arg" and writes the output to http.ResponseWriter.
|
||||||
|
// It logs a warning in case of error.
|
||||||
func (c *controller) render(rw http.ResponseWriter, tmpl string, arg interface{}) {
|
func (c *controller) render(rw http.ResponseWriter, tmpl string, arg interface{}) {
|
||||||
rw.Header().Set("Content-type", "text/html; charset=utf-8")
|
rw.Header().Set("Content-type", "text/html; charset=utf-8")
|
||||||
rw.Header().Set("X-Content-Type-Options", "nosniff")
|
rw.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
@ -101,17 +112,24 @@ func (c *controller) render(rw http.ResponseWriter, tmpl string, arg interface{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// failed constructs the error messages by calling "asMultiError" and calls "render"
|
||||||
|
// function to render the passed template and error object.
|
||||||
func (c *controller) failed(rw http.ResponseWriter, tmpl string, err error) {
|
func (c *controller) failed(rw http.ResponseWriter, tmpl string, err error) {
|
||||||
result := map[string]interface{}{"Error": asMultiError(err)}
|
result := map[string]interface{}{"Error": asMultiError(err)}
|
||||||
c.render(rw, tmpl, result)
|
c.render(rw, tmpl, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// index calls the "render" function and passes the "index.html" and c.cfg to it.
|
||||||
func (c *controller) index(rw http.ResponseWriter, r *http.Request) {
|
func (c *controller) index(rw http.ResponseWriter, r *http.Request) {
|
||||||
c.render(rw, "index.html", map[string]interface{}{
|
c.render(rw, "index.html", map[string]interface{}{
|
||||||
"Config": c.cfg,
|
"Config": c.cfg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// web executes the given function "fn", calls the "render" function and passes
|
||||||
|
// the result content from "fn", the given template and the http.ResponseWriter to it
|
||||||
|
// in case of no error occurred, otherwise calls the "failed" function and passes the given
|
||||||
|
// template and the error from "fn".
|
||||||
func (c *controller) web(
|
func (c *controller) web(
|
||||||
fn func(*http.Request) (interface{}, error),
|
fn func(*http.Request) (interface{}, error),
|
||||||
tmpl string,
|
tmpl string,
|
||||||
|
|
@ -126,6 +144,8 @@ func (c *controller) web(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeJSON sets the header for the response and writes the JSON encoding of the given "content".
|
||||||
|
// It logs out an error message in case of an error.
|
||||||
func writeJSON(rw http.ResponseWriter, content interface{}, code int) {
|
func writeJSON(rw http.ResponseWriter, content interface{}, code int) {
|
||||||
rw.Header().Set("Content-type", "application/json; charset=utf-8")
|
rw.Header().Set("Content-type", "application/json; charset=utf-8")
|
||||||
rw.Header().Set("X-Content-Type-Options", "nosniff")
|
rw.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import (
|
||||||
"github.com/csaf-poc/csaf_distribution/util"
|
"github.com/csaf-poc/csaf_distribution/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ensureFolders initializes the paths and call functions to create
|
||||||
|
// the directories and files.
|
||||||
func ensureFolders(c *config) error {
|
func ensureFolders(c *config) error {
|
||||||
|
|
||||||
wellknown := filepath.Join(c.Web, ".well-known")
|
wellknown := filepath.Join(c.Web, ".well-known")
|
||||||
|
|
@ -38,6 +40,8 @@ func ensureFolders(c *config) error {
|
||||||
return createSecurity(c, wellknown)
|
return createSecurity(c, wellknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createWellknown creates ".well-known" directory if not exist and returns nil.
|
||||||
|
// An error is returned if the it is not a directory.
|
||||||
func createWellknown(wellknown string) error {
|
func createWellknown(wellknown string) error {
|
||||||
st, err := os.Stat(wellknown)
|
st, err := os.Stat(wellknown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -52,6 +56,10 @@ func createWellknown(wellknown string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createFeedFolders creates the feed folders according to the tlp values
|
||||||
|
// in the "tlps" config option if they do not already exist.
|
||||||
|
// No creation for the "csaf" option will be done.
|
||||||
|
// It creates also symbolic links to feed folders.
|
||||||
func createFeedFolders(c *config, wellknown string) error {
|
func createFeedFolders(c *config, wellknown string) error {
|
||||||
for _, t := range c.TLPs {
|
for _, t := range c.TLPs {
|
||||||
if t == tlpCSAF {
|
if t == tlpCSAF {
|
||||||
|
|
@ -75,6 +83,8 @@ func createFeedFolders(c *config, wellknown string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createSecurity creats the "security.txt" file if does not exist
|
||||||
|
// and writes the CSAF field inside the file.
|
||||||
func createSecurity(c *config, wellknown string) error {
|
func createSecurity(c *config, wellknown string) error {
|
||||||
security := filepath.Join(wellknown, "security.txt")
|
security := filepath.Join(wellknown, "security.txt")
|
||||||
if _, err := os.Stat(security); err != nil {
|
if _, err := os.Stat(security); err != nil {
|
||||||
|
|
@ -93,6 +103,7 @@ func createSecurity(c *config, wellknown string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createProviderMetadata creates the provider-metadata.json file if does not exist.
|
||||||
func createProviderMetadata(c *config, wellknownCSAF string) error {
|
func createProviderMetadata(c *config, wellknownCSAF string) error {
|
||||||
path := filepath.Join(wellknownCSAF, "provider-metadata.json")
|
path := filepath.Join(wellknownCSAF, "provider-metadata.json")
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
//
|
//
|
||||||
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
||||||
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
|
// Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
|
||||||
|
|
||||||
// Implemnts a command line tool that loads csaf documents to a trusted provider
|
// Implements a command line tool that uploads csaf documents to csaf_provider.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -34,7 +34,7 @@ type options struct {
|
||||||
Action string `short:"a" long:"action" choice:"upload" choice:"create" default:"upload" description:"Action to perform"`
|
Action string `short:"a" long:"action" choice:"upload" choice:"create" default:"upload" description:"Action to perform"`
|
||||||
URL string `short:"u" long:"url" description:"URL of the CSAF provider" default:"https://localhost/cgi-bin/csaf_provider.go" value-name:"URL"`
|
URL string `short:"u" long:"url" description:"URL of the CSAF provider" default:"https://localhost/cgi-bin/csaf_provider.go" value-name:"URL"`
|
||||||
TLP string `short:"t" long:"tlp" choice:"csaf" choice:"white" choice:"green" choice:"amber" choice:"red" default:"csaf" description:"TLP of the feed"`
|
TLP string `short:"t" long:"tlp" choice:"csaf" choice:"white" choice:"green" choice:"amber" choice:"red" default:"csaf" description:"TLP of the feed"`
|
||||||
ExternalSigned bool `short:"x" long:"external-signed" description:"CASF files are signed externally. Assumes .asc files beside CSAF files."`
|
ExternalSigned bool `short:"x" long:"external-signed" description:"CSAF files are signed externally. Assumes .asc files beside CSAF files."`
|
||||||
NoSchemaCheck bool `short:"s" long:"no-schema-check" description:"Do not check files against CSAF JSON schema locally."`
|
NoSchemaCheck bool `short:"s" long:"no-schema-check" description:"Do not check files against CSAF JSON schema locally."`
|
||||||
|
|
||||||
Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"`
|
Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"`
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
//
|
//
|
||||||
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
||||||
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
|
// Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
|
||||||
|
|
||||||
package csaf
|
package csaf
|
||||||
|
|
||||||
|
|
@ -72,13 +72,13 @@ type Distribution struct {
|
||||||
type TimeStamp time.Time
|
type TimeStamp time.Time
|
||||||
|
|
||||||
// Fingerprint is the fingerprint of a OpenPGP key used to sign
|
// Fingerprint is the fingerprint of a OpenPGP key used to sign
|
||||||
// the CASF documents.
|
// the CSAF documents.
|
||||||
type Fingerprint string
|
type Fingerprint string
|
||||||
|
|
||||||
var fingerprintPattern = patternUnmarshal(`^[0-9a-fA-F]{40,}$`)
|
var fingerprintPattern = patternUnmarshal(`^[0-9a-fA-F]{40,}$`)
|
||||||
|
|
||||||
// PGPKey is location and the fingerprint of the key
|
// PGPKey is location and the fingerprint of the key
|
||||||
// used to sign the CASF documents.
|
// used to sign the CSAF documents.
|
||||||
type PGPKey struct {
|
type PGPKey struct {
|
||||||
Fingerprint Fingerprint `json:"fingerprint,omitempty"`
|
Fingerprint Fingerprint `json:"fingerprint,omitempty"`
|
||||||
URL *string `json:"url"` // required
|
URL *string `json:"url"` // required
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,8 @@ type Entry struct {
|
||||||
Format Format `json:"format"`
|
Format Format `json:"format"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ROLIEFeed is a ROLIE feed.
|
// FeedData is the content of the ROLIE feed.
|
||||||
type ROLIEFeed struct {
|
type FeedData struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Link []Link `json:"link,omitempty"`
|
Link []Link `json:"link,omitempty"`
|
||||||
|
|
@ -68,6 +68,11 @@ type ROLIEFeed struct {
|
||||||
Entry []*Entry `json:"entry,omitempty"`
|
Entry []*Entry `json:"entry,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ROLIEFeed is a ROLIE feed.
|
||||||
|
type ROLIEFeed struct {
|
||||||
|
Feed FeedData `json:"feed"`
|
||||||
|
}
|
||||||
|
|
||||||
// LoadROLIEFeed loads a ROLIE feed from a reader.
|
// LoadROLIEFeed loads a ROLIE feed from a reader.
|
||||||
func LoadROLIEFeed(r io.Reader) (*ROLIEFeed, error) {
|
func LoadROLIEFeed(r io.Reader) (*ROLIEFeed, error) {
|
||||||
dec := json.NewDecoder(r)
|
dec := json.NewDecoder(r)
|
||||||
|
|
@ -90,7 +95,7 @@ func (rf *ROLIEFeed) WriteTo(w io.Writer) (int64, error) {
|
||||||
// EntryByID looks up an entry by its ID.
|
// EntryByID looks up an entry by its ID.
|
||||||
// Returns nil if no such entry was found.
|
// Returns nil if no such entry was found.
|
||||||
func (rf *ROLIEFeed) EntryByID(id string) *Entry {
|
func (rf *ROLIEFeed) EntryByID(id string) *Entry {
|
||||||
for _, entry := range rf.Entry {
|
for _, entry := range rf.Feed.Entry {
|
||||||
if entry.ID == id {
|
if entry.ID == id {
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +106,7 @@ func (rf *ROLIEFeed) EntryByID(id string) *Entry {
|
||||||
// SortEntriesByUpdated sorts all the entries in the feed
|
// SortEntriesByUpdated sorts all the entries in the feed
|
||||||
// by their update times.
|
// by their update times.
|
||||||
func (rf *ROLIEFeed) SortEntriesByUpdated() {
|
func (rf *ROLIEFeed) SortEntriesByUpdated() {
|
||||||
entries := rf.Entry
|
entries := rf.Feed.Entry
|
||||||
sort.Slice(entries, func(i, j int) bool {
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
return time.Time(entries[j].Updated).Before(time.Time(entries[i].Updated))
|
return time.Time(entries[j].Updated).Before(time.Time(entries[i].Updated))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,12 @@
|
||||||
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
||||||
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
|
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
|
||||||
|
|
||||||
package main
|
package csaf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/PaesslerAG/gval"
|
|
||||||
"github.com/PaesslerAG/jsonpath"
|
|
||||||
|
|
||||||
"github.com/csaf-poc/csaf_distribution/csaf"
|
|
||||||
"github.com/csaf-poc/csaf_distribution/util"
|
"github.com/csaf-poc/csaf_distribution/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -30,39 +25,39 @@ const (
|
||||||
summaryExpr = `$.document.notes[? @.category=="summary" || @.type=="summary"].text`
|
summaryExpr = `$.document.notes[? @.category=="summary" || @.type=="summary"].text`
|
||||||
)
|
)
|
||||||
|
|
||||||
type extraction struct {
|
// AdvisorySummary is a summary of some essentials of an CSAF advisory.
|
||||||
id string
|
type AdvisorySummary struct {
|
||||||
title string
|
ID string
|
||||||
publisher *csaf.Publisher
|
Title string
|
||||||
initialReleaseDate time.Time
|
Publisher *Publisher
|
||||||
currentReleaseDate time.Time
|
InitialReleaseDate time.Time
|
||||||
summary string
|
CurrentReleaseDate time.Time
|
||||||
tlpLabel string
|
Summary string
|
||||||
|
TLPLabel string
|
||||||
}
|
}
|
||||||
|
|
||||||
type extractFunc func(string) (interface{}, error)
|
type extractFunc func(string) (interface{}, error)
|
||||||
|
|
||||||
func newExtraction(content interface{}) (*extraction, error) {
|
// NewAdvisorySummary creates a summary from an advisory doc
|
||||||
|
// with the help of an expression evaluator expr.
|
||||||
|
func NewAdvisorySummary(
|
||||||
|
expr *util.PathEval,
|
||||||
|
doc interface{},
|
||||||
|
) (*AdvisorySummary, error) {
|
||||||
|
|
||||||
builder := gval.Full(jsonpath.Language())
|
e := new(AdvisorySummary)
|
||||||
|
|
||||||
path := func(expr string) (interface{}, error) {
|
path := func(s string) (interface{}, error) {
|
||||||
eval, err := builder.NewEvaluable(expr)
|
return expr.Eval(s, doc)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return eval(context.Background(), content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
e := new(extraction)
|
|
||||||
|
|
||||||
for _, fn := range []func(extractFunc) error{
|
for _, fn := range []func(extractFunc) error{
|
||||||
extractText(idExpr, &e.id),
|
extractText(idExpr, &e.ID),
|
||||||
extractText(titleExpr, &e.title),
|
extractText(titleExpr, &e.Title),
|
||||||
extractTime(currentReleaseDateExpr, &e.currentReleaseDate),
|
extractTime(currentReleaseDateExpr, &e.CurrentReleaseDate),
|
||||||
extractTime(initialReleaseDateExpr, &e.initialReleaseDate),
|
extractTime(initialReleaseDateExpr, &e.InitialReleaseDate),
|
||||||
extractText(summaryExpr, &e.summary),
|
extractText(summaryExpr, &e.Summary),
|
||||||
extractText(tlpLabelExpr, &e.tlpLabel),
|
extractText(tlpLabelExpr, &e.TLPLabel),
|
||||||
e.extractPublisher,
|
e.extractPublisher,
|
||||||
} {
|
} {
|
||||||
if err := fn(path); err != nil {
|
if err := fn(path); err != nil {
|
||||||
|
|
@ -95,7 +90,7 @@ func extractTime(expr string, store *time.Time) func(extractFunc) error {
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("not a string")
|
return errors.New("not a string")
|
||||||
}
|
}
|
||||||
date, err := time.Parse(dateFormat, text)
|
date, err := time.Parse(time.RFC3339, text)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
*store = date.UTC()
|
*store = date.UTC()
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +98,7 @@ func extractTime(expr string, store *time.Time) func(extractFunc) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *extraction) extractPublisher(path extractFunc) error {
|
func (e *AdvisorySummary) extractPublisher(path extractFunc) error {
|
||||||
p, err := path(publisherExpr)
|
p, err := path(publisherExpr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -111,13 +106,13 @@ func (e *extraction) extractPublisher(path extractFunc) error {
|
||||||
|
|
||||||
// XXX: It's a bit cumbersome to serialize and deserialize
|
// XXX: It's a bit cumbersome to serialize and deserialize
|
||||||
// it into our own structure.
|
// it into our own structure.
|
||||||
publisher := new(csaf.Publisher)
|
publisher := new(Publisher)
|
||||||
if err := util.ReMarshalJSON(publisher, p); err != nil {
|
if err := util.ReMarshalJSON(publisher, p); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := publisher.Validate(); err != nil {
|
if err := publisher.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
e.publisher = publisher
|
e.Publisher = publisher
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
87
docs/development-ca.md
Normal file
87
docs/development-ca.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Certificate Authority for development purposes
|
||||||
|
|
||||||
|
A bare bones development certificate authority (CA) can be set up
|
||||||
|
to create certs for serving TLS connections.
|
||||||
|
|
||||||
|
Install GnuTLS, E.g. with `apt install gnutls-bin` (3.7.1-5) on Debian Bullseye.
|
||||||
|
|
||||||
|
All the private keys will be created without password protection,
|
||||||
|
which is suitable for testing in development setups.
|
||||||
|
|
||||||
|
|
||||||
|
## create root CA
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir devca1
|
||||||
|
cd devca1
|
||||||
|
|
||||||
|
certtool --generate-privkey --outfile rootca-key.pem
|
||||||
|
|
||||||
|
echo '
|
||||||
|
organization = "CSAF Tools Development (internal)"
|
||||||
|
country = DE
|
||||||
|
cn = "Tester"
|
||||||
|
|
||||||
|
ca
|
||||||
|
cert_signing_key
|
||||||
|
crl_signing_key
|
||||||
|
|
||||||
|
serial = 001
|
||||||
|
expiration_days = 100
|
||||||
|
' >gnutls-certtool.rootca.template
|
||||||
|
|
||||||
|
certtool --generate-self-signed --load-privkey rootca-key.pem --outfile rootca-cert.pem --template gnutls-certtool.rootca.template
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## create webserver cert
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#being in devca1/
|
||||||
|
|
||||||
|
certtool --generate-privkey --outfile testserver-key.pem
|
||||||
|
|
||||||
|
echo '
|
||||||
|
organization = "CSAF Tools Development (internal)"
|
||||||
|
country = DE
|
||||||
|
cn = "Service Testing"
|
||||||
|
|
||||||
|
tls_www_server
|
||||||
|
signing_key
|
||||||
|
encryption_key
|
||||||
|
non_repudiation
|
||||||
|
|
||||||
|
dns_name = "*.local"
|
||||||
|
dns_name = "localhost"
|
||||||
|
|
||||||
|
serial = 010
|
||||||
|
expiration_days = 50
|
||||||
|
' > gnutls-certtool.testserver.template
|
||||||
|
|
||||||
|
certtool --generate-certificate --load-privkey testserver-key.pem --outfile testserver.crt --load-ca-certificate rootca-cert.pem --load-ca-privkey rootca-key.pem --template gnutls-certtool.testserver.template
|
||||||
|
|
||||||
|
cat testserver.crt rootca-cert.pem >bundle.crt
|
||||||
|
echo Full path config options for nginx:
|
||||||
|
echo " ssl_certificate \"$PWD/bundle.crt\";"
|
||||||
|
echo " ssl_certificate_key \"$PWD/testserver-key.pem\";"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Considerations and References
|
||||||
|
|
||||||
|
* The command line and template options are explained in the
|
||||||
|
GnuTLS documentation at the end of _certtool Invocation_, see the [section of the current stable documentation](https://gnutls.org/manual/html_node/certtool-Invocation.html), but be aware that it maybe newer than
|
||||||
|
the version you have installed.
|
||||||
|
* Using GnuTLS instead of OpenSSL, because GnuTLS is an implementation
|
||||||
|
with a long, good track record. Configuration is also slightly slimmer.
|
||||||
|
(Overall it is positive for the security of Open Standards
|
||||||
|
like TLS and CMS, that there are several competing compatible
|
||||||
|
implementations. Selecting a different implementation here and there helps
|
||||||
|
the ecosystem by fostering that competition.)
|
||||||
|
* Using the GnuTLS default algorithm (RSA 3072 at time for writing) is
|
||||||
|
good enough, as the goal is not to test ECC compatibility for client
|
||||||
|
certificates for servers, browser and tools.
|
||||||
|
* An example script for server certs:
|
||||||
|
https://gist.github.com/epcim/832cec2482a255e3f392
|
||||||
|
* An example for client certs as part of the libvirt setup instructions:
|
||||||
|
https://wiki.libvirt.org/page/TLSCreateClientCerts
|
||||||
|
|
@ -14,9 +14,11 @@ There are three ways to get a TLS certificate for your HTTPS server:
|
||||||
[Let's encrypt](https://letsencrypt.org/) without a fee.
|
[Let's encrypt](https://letsencrypt.org/) without a fee.
|
||||||
See their instruction, e.g.
|
See their instruction, e.g.
|
||||||
[certbot for nignx on Ubuntu](https://certbot.eff.org/instructions?ws=nginx&os=ubuntufocal).
|
[certbot for nignx on Ubuntu](https://certbot.eff.org/instructions?ws=nginx&os=ubuntufocal).
|
||||||
3. Run your own little CA. Which has the major drawback that someone
|
3. [Run your own little CA](development-ca.md).
|
||||||
will have to import the root certificate in the webbrowsers manually.
|
Which has the major drawback that someone
|
||||||
Suitable for development purposes.
|
will have to import the root certificate in the webbrowsers manually or
|
||||||
|
override warning on each connect.
|
||||||
|
Suitable for development purposes, must not be used for production servers.
|
||||||
|
|
||||||
To decide between 1. and 2. you will need to weight the extra
|
To decide between 1. and 2. you will need to weight the extra
|
||||||
efforts and costs of the level of extended validation against
|
efforts and costs of the level of extended validation against
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ server {
|
||||||
include fcgiwrap.conf;
|
include fcgiwrap.conf;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
Reload nginx to apply the changes (e.g. ```systemctl reload nginx``` on Debian or Ubuntu).
|
||||||
|
|
||||||
Place the binary under `/usr/lib/cgi-bin/csaf_provider.go`.
|
Place the binary under `/usr/lib/cgi-bin/csaf_provider.go`.
|
||||||
Make sure `/usr/lib/cgi-bin/` exists.
|
Make sure `/usr/lib/cgi-bin/` exists.
|
||||||
|
|
@ -90,8 +91,10 @@ key = "/usr/lib/csaf/private.asc"
|
||||||
domain = "http://192.168.56.102"
|
domain = "http://192.168.56.102"
|
||||||
#no_passphrase = true
|
#no_passphrase = true
|
||||||
```
|
```
|
||||||
|
with suitable replacements
|
||||||
|
(This configurations-example assumes that the private/public keys are available under `/usr/lib/csaf/`).
|
||||||
|
|
||||||
|
|
||||||
with suitable replacements.
|
|
||||||
|
|
||||||
Create the folders:
|
Create the folders:
|
||||||
```(shell)
|
```(shell)
|
||||||
|
|
|
||||||
30
util/file_test.go
Normal file
30
util/file_test.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNWriter(t *testing.T) {
|
||||||
|
|
||||||
|
msg := []byte("Gruß!\n")
|
||||||
|
|
||||||
|
first, second := msg[:len(msg)/2], msg[len(msg)/2:]
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
nw := NWriter{Writer: &buf, N: 0}
|
||||||
|
_, err1 := nw.Write(first)
|
||||||
|
_, err2 := nw.Write(second)
|
||||||
|
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
t.Error("Calling NWriter failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if n := int64(len(msg)); nw.N != n {
|
||||||
|
t.Errorf("Expected %d bytes, but counted %d.", n, nw.N)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out := buf.Bytes(); !bytes.Equal(msg, out) {
|
||||||
|
t.Errorf("Expected %q, but got %q", msg, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
36
util/json.go
36
util/json.go
|
|
@ -9,7 +9,12 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/PaesslerAG/gval"
|
||||||
|
"github.com/PaesslerAG/jsonpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReMarshalJSON transforms data from src to dst via JSON marshalling.
|
// ReMarshalJSON transforms data from src to dst via JSON marshalling.
|
||||||
|
|
@ -20,3 +25,34 @@ func ReMarshalJSON(dst, src interface{}) error {
|
||||||
}
|
}
|
||||||
return json.Unmarshal(intermediate, dst)
|
return json.Unmarshal(intermediate, dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PathEval is a helper to evaluate JSON paths on documents.
|
||||||
|
type PathEval struct {
|
||||||
|
builder gval.Language
|
||||||
|
exprs map[string]gval.Evaluable
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPathEval creates a new PathEval.
|
||||||
|
func NewPathEval() *PathEval {
|
||||||
|
return &PathEval{
|
||||||
|
builder: gval.Full(jsonpath.Language()),
|
||||||
|
exprs: map[string]gval.Evaluable{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eval evalutes expression expr on document doc.
|
||||||
|
// Returns the result of the expression.
|
||||||
|
func (pe *PathEval) Eval(expr string, doc interface{}) (interface{}, error) {
|
||||||
|
if doc == nil {
|
||||||
|
return nil, errors.New("no document to extract data from")
|
||||||
|
}
|
||||||
|
eval := pe.exprs[expr]
|
||||||
|
if eval == nil {
|
||||||
|
var err error
|
||||||
|
if eval, err = pe.builder.NewEvaluable(expr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pe.exprs[expr] = eval
|
||||||
|
}
|
||||||
|
return eval(context.Background(), doc)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue