diff --git a/cmd/image-inspector.go b/cmd/image-inspector.go index 06b8e53..0c9003f 100644 --- a/cmd/image-inspector.go +++ b/cmd/image-inspector.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + iiapi "github.com/openshift/image-inspector/pkg/api" iicmd "github.com/openshift/image-inspector/pkg/cmd" ii "github.com/openshift/image-inspector/pkg/inspector" ) @@ -20,7 +21,7 @@ func main() { flag.Var(&inspectorOptions.DockerCfg, "dockercfg", "Location of the docker configuration files. May be specified more than once") flag.StringVar(&inspectorOptions.Username, "username", inspectorOptions.Username, "username for authenticating with the docker registry") flag.StringVar(&inspectorOptions.PasswordFile, "password-file", inspectorOptions.PasswordFile, "Location of a file that contains the password for authentication with the docker registry") - flag.StringVar(&inspectorOptions.ScanType, "scan-type", inspectorOptions.ScanType, fmt.Sprintf("The type of the scan to be done on the inspected image. Available scan types are: %v", iicmd.ScanOptions)) + flag.StringVar(&inspectorOptions.ScanType, "scan-type", inspectorOptions.ScanType, fmt.Sprintf("The type of the scan to be done on the inspected image. Available scan types are: %v", iiapi.ScanOptions)) flag.StringVar(&inspectorOptions.ScanResultsDir, "scan-results-dir", inspectorOptions.ScanResultsDir, "The directory that will contain the results of the scan") flag.BoolVar(&inspectorOptions.OpenScapHTML, "openscap-html-report", inspectorOptions.OpenScapHTML, "Generate an OpenScap HTML report in addition to the ARF formatted report") flag.StringVar(&inspectorOptions.CVEUrlPath, "cve-url", inspectorOptions.CVEUrlPath, "An alternative URL source for CVE files") diff --git a/pkg/api/types.go b/pkg/api/types.go new file mode 100644 index 0000000..638b6c9 --- /dev/null +++ b/pkg/api/types.go @@ -0,0 +1,44 @@ +package api + +import ( + docker "github.com/fsouza/go-dockerclient" + "time" +) + +// OpenSCAPStatus is the status of openscap scan +type OpenSCAPStatus string + +const ( + StatusNotRequested OpenSCAPStatus = "NotRequested" + StatusSuccess OpenSCAPStatus = "Success" + StatusError OpenSCAPStatus = "Error" +) + +type OpenSCAPMetadata struct { + Status OpenSCAPStatus // Status of the OpenSCAP scan report + ErrorMessage string // Error message from the openscap + ContentTimeStamp string // Timestamp for this data +} + +func (osm *OpenSCAPMetadata) SetError(err error) { + osm.Status = StatusError + osm.ErrorMessage = err.Error() + osm.ContentTimeStamp = string(time.Now().Format(time.RFC850)) +} + +var ( + ScanOptions = []string{"openscap"} +) + +// InspectorMetadata is the metadata type with information about image-inspector's operation +type InspectorMetadata struct { + docker.Image // Metadata about the inspected image + // OpenSCAP describes the state of the OpenSCAP scan + OpenSCAP *OpenSCAPMetadata +} + +// APIVersions holds a slice of supported API versions. +type APIVersions struct { + // Versions is the supported API versions + Versions []string `json:"versions"` +} diff --git a/pkg/cmd/types.go b/pkg/cmd/types.go index 769cfc1..f4b774d 100644 --- a/pkg/cmd/types.go +++ b/pkg/cmd/types.go @@ -2,12 +2,12 @@ package cmd import ( "fmt" + oscapscanner "github.com/openshift/image-inspector/pkg/openscap" - "os" -) -var ( - ScanOptions = []string{"openscap"} + iiapi "github.com/openshift/image-inspector/pkg/api" + + "os" ) // MultiStringVar is implementing flag.Value @@ -110,14 +110,15 @@ func (i *ImageInspectorOptions) Validate() error { } if len(i.ScanType) > 0 { var found bool = false - for _, opt := range ScanOptions { + for _, opt := range iiapi.ScanOptions { if i.ScanType == opt { found = true break } } if !found { - return fmt.Errorf("%s is not one of the available scan-types which are %v", i.ScanType, ScanOptions) + return fmt.Errorf("%s is not one of the available scan-types which are %v", + i.ScanType, iiapi.ScanOptions) } } diff --git a/pkg/imageserver/types.go b/pkg/imageserver/types.go new file mode 100644 index 0000000..6efd8c4 --- /dev/null +++ b/pkg/imageserver/types.go @@ -0,0 +1,41 @@ +package imageserver + +import ( + iiapi "github.com/openshift/image-inspector/pkg/api" +) + +// ImageServer abstracts the serving of image information. +type ImageServer interface { + // ServeImage Serves the image + ServeImage(meta *iiapi.InspectorMetadata, + scanReport []byte, + htmlScanReport []byte) error +} + +// ImageServerOptions is used to configure an image server. +type ImageServerOptions struct { + // ServePath is the root path/port of serving. ex 0.0.0.0:8080 + ServePath string + // HealthzURL is the relative url of the health check. ex /healthz + HealthzURL string + // APIURL is the relative url where the api will be served. ex /api + APIURL string + // APIVersions are the supported API versions. + APIVersions iiapi.APIVersions + // MetadataURL is the relative url of the metadata content. ex /api/v1/metadata + MetadataURL string + // ContentURL is the relative url of the content. ex /api/v1/content/ + ContentURL string + // ImageServeURL is the location that the image is being served from. + // NOTE: if the image server supports a chroot the server implementation will perform + // the chroot based on this URL. + ImageServeURL string + // ScanType is the type of the scan that was done on the inspected image + ScanType string + // ScanReportURL is the url to publish the scan report + ScanReportURL string + // HTMLScanReport wether or not to publish an HTML scan report + HTMLScanReport bool + // HTMLScanReportURL url for the scan html report + HTMLScanReportURL string +} diff --git a/pkg/imageserver/webdav.go b/pkg/imageserver/webdav.go new file mode 100644 index 0000000..8b7ef1f --- /dev/null +++ b/pkg/imageserver/webdav.go @@ -0,0 +1,112 @@ +package imageserver + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "syscall" + + "golang.org/x/net/webdav" + + iiapi "github.com/openshift/image-inspector/pkg/api" +) + +const ( + // CHROOT_SERVE_PATH is the path to server if we are performing a chroot + // this probably does not belong here. + CHROOT_SERVE_PATH = "/" +) + +// webdavImageServer implements ImageServer. +type webdavImageServer struct { + opts ImageServerOptions + chroot bool +} + +// ensures this always implements the interface or fail compilation. +var _ ImageServer = &webdavImageServer{} + +// NewWebdavImageServer creates a new webdav image server. +func NewWebdavImageServer(opts ImageServerOptions, chroot bool) ImageServer { + return &webdavImageServer{ + opts: opts, + chroot: chroot, + } +} + +// ServeImage Serves the image. +func (s *webdavImageServer) ServeImage(meta *iiapi.InspectorMetadata, + scanReport []byte, + htmlScanReport []byte) error { + + servePath := s.opts.ImageServeURL + if s.chroot { + if err := syscall.Chroot(s.opts.ImageServeURL); err != nil { + return fmt.Errorf("Unable to chroot into %s: %v\n", s.opts.ImageServeURL, err) + } + servePath = CHROOT_SERVE_PATH + } else { + log.Printf("!!!WARNING!!! It is insecure to serve the image content without changing") + log.Printf("root (--chroot). Absolute-path symlinks in the image can lead to disclose") + log.Printf("information of the hosting system.") + } + + log.Printf("Serving image content %s on webdav://%s%s", s.opts.ImageServeURL, s.opts.ServePath, s.opts.ContentURL) + + http.HandleFunc(s.opts.HealthzURL, func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok\n")) + }) + + http.HandleFunc(s.opts.APIURL, func(w http.ResponseWriter, r *http.Request) { + body, err := json.MarshalIndent(s.opts.APIVersions, "", " ") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(body) + }) + + http.HandleFunc(s.opts.MetadataURL, func(w http.ResponseWriter, r *http.Request) { + body, err := json.MarshalIndent(meta, "", " ") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(body) + }) + + http.HandleFunc(s.opts.ScanReportURL, func(w http.ResponseWriter, r *http.Request) { + if s.opts.ScanType != "" && meta.OpenSCAP.Status == iiapi.StatusSuccess { + w.Write(scanReport) + } else { + if meta.OpenSCAP.Status == iiapi.StatusError { + http.Error(w, fmt.Sprintf("OpenSCAP Error: %s", meta.OpenSCAP.ErrorMessage), + http.StatusInternalServerError) + } else { + http.Error(w, "OpenSCAP option was not chosen", http.StatusNotFound) + } + } + }) + + http.HandleFunc(s.opts.HTMLScanReportURL, func(w http.ResponseWriter, r *http.Request) { + if s.opts.ScanType != "" && meta.OpenSCAP.Status == iiapi.StatusSuccess && s.opts.HTMLScanReport { + w.Write(htmlScanReport) + } else { + if meta.OpenSCAP.Status == iiapi.StatusError { + http.Error(w, fmt.Sprintf("OpenSCAP Error: %s", meta.OpenSCAP.ErrorMessage), + http.StatusInternalServerError) + } else { + http.Error(w, "OpenSCAP option was not chosen", http.StatusNotFound) + } + } + }) + + http.Handle(s.opts.ContentURL, &webdav.Handler{ + Prefix: s.opts.ContentURL, + FileSystem: webdav.Dir(servePath), + LockSystem: webdav.NewMemLS(), + }) + + return http.ListenAndServe(s.opts.ServePath, nil) +} diff --git a/pkg/inspector/image-inspector.go b/pkg/inspector/image-inspector.go index 3136be8..34ef3b4 100644 --- a/pkg/inspector/image-inspector.go +++ b/pkg/inspector/image-inspector.go @@ -8,11 +8,9 @@ import ( "log" "math" "math/big" - "net/http" "os" "path" "strings" - "syscall" "time" "archive/tar" @@ -20,14 +18,12 @@ import ( docker "github.com/fsouza/go-dockerclient" "github.com/openshift/image-inspector/pkg/openscap" - "golang.org/x/net/webdav" iicmd "github.com/openshift/image-inspector/pkg/cmd" -) -type APIVersions struct { - Versions []string `json:"versions"` -} + iiapi "github.com/openshift/image-inspector/pkg/api" + apiserver "github.com/openshift/image-inspector/pkg/imageserver" +) const ( VERSION_TAG = "v1" @@ -56,13 +52,49 @@ type ImageInspector interface { // defaultImageInspector is the default implementation of ImageInspector. type defaultImageInspector struct { opts iicmd.ImageInspectorOptions - meta InspectorMetadata + meta iiapi.InspectorMetadata + // an optional image server that will server content for inspection. + imageServer apiserver.ImageServer +} + +// NewInspectorMetadata returns a new InspectorMetadata out of *docker.Image +// The OpenSCAP status will be NotRequested +func NewInspectorMetadata(imageMetadata *docker.Image) iiapi.InspectorMetadata { + return iiapi.InspectorMetadata{ + Image: *imageMetadata, + OpenSCAP: &iiapi.OpenSCAPMetadata{ + Status: iiapi.StatusNotRequested, + ErrorMessage: "", + ContentTimeStamp: string(time.Now().Format(time.RFC850)), + }, + } } // NewDefaultImageInspector provides a new default inspector. func NewDefaultImageInspector(opts iicmd.ImageInspectorOptions) ImageInspector { - return &defaultImageInspector{opts, - *NewInspectorMetadata(&docker.Image{})} + inspector := &defaultImageInspector{ + opts: opts, + meta: NewInspectorMetadata(&docker.Image{}), + } + + // if serving then set up an image server + if len(opts.Serve) > 0 { + imageServerOpts := apiserver.ImageServerOptions{ + ServePath: opts.Serve, + HealthzURL: HEALTHZ_URL_PATH, + APIURL: API_URL_PREFIX, + APIVersions: iiapi.APIVersions{Versions: []string{VERSION_TAG}}, + MetadataURL: METADATA_URL_PATH, + ContentURL: CONTENT_URL_PREFIX, + ImageServeURL: opts.DstPath, + ScanType: opts.ScanType, + ScanReportURL: OPENSCAP_URL_PATH, + HTMLScanReport: opts.OpenScapHTML, + HTMLScanReportURL: OPENSCAP_REPORT_URL_PATH, + } + inspector.imageServer = apiserver.NewWebdavImageServer(imageServerOpts, opts.Chroot) + } + return inspector } // Inspect inspects and serves the image based on the ImageInspectorOptions. @@ -87,8 +119,6 @@ func (i *defaultImageInspector) Inspect() error { } i.meta.Image = *imageMetadata - supportedVersions := APIVersions{Versions: []string{VERSION_TAG}} - var scanReport []byte var htmlScanReport []byte if i.opts.ScanType == "openscap" { @@ -101,80 +131,13 @@ func (i *defaultImageInspector) Inspect() error { i.meta.OpenSCAP.SetError(err) log.Printf("Unable to scan image: %v", err) } else { - i.meta.OpenSCAP.Status = StatusSuccess + i.meta.OpenSCAP.Status = iiapi.StatusSuccess } } - if len(i.opts.Serve) > 0 { - servePath := i.opts.DstPath - if i.opts.Chroot { - if err := syscall.Chroot(i.opts.DstPath); err != nil { - return fmt.Errorf("Unable to chroot into %s: %v\n", i.opts.DstPath, err) - } - servePath = CHROOT_SERVE_PATH - } else { - log.Printf("!!!WARNING!!! It is insecure to serve the image content without changing") - log.Printf("root (--chroot). Absolute-path symlinks in the image can lead to disclose") - log.Printf("information of the hosting system.") - } - - log.Printf("Serving image content %s on webdav://%s%s", i.opts.DstPath, i.opts.Serve, CONTENT_URL_PREFIX) - - http.HandleFunc(HEALTHZ_URL_PATH, func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("ok\n")) - }) - - http.HandleFunc(API_URL_PREFIX, func(w http.ResponseWriter, r *http.Request) { - body, err := json.MarshalIndent(supportedVersions, "", " ") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(body) - }) - - http.HandleFunc(METADATA_URL_PATH, func(w http.ResponseWriter, r *http.Request) { - body, err := json.MarshalIndent(i.meta, "", " ") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(body) - }) - - http.HandleFunc(OPENSCAP_URL_PATH, func(w http.ResponseWriter, r *http.Request) { - if i.opts.ScanType == "openscap" && i.meta.OpenSCAP.Status == StatusSuccess { - w.Write(scanReport) - } else { - if i.meta.OpenSCAP.Status == StatusError { - http.Error(w, fmt.Sprintf("OpenSCAP Error: %s", i.meta.OpenSCAP.ErrorMessage), - http.StatusInternalServerError) - } else { - http.Error(w, "OpenSCAP option was not chosen", http.StatusNotFound) - } - } - }) - - http.HandleFunc(OPENSCAP_REPORT_URL_PATH, func(w http.ResponseWriter, r *http.Request) { - if i.opts.ScanType == "openscap" && i.meta.OpenSCAP.Status == StatusSuccess && i.opts.OpenScapHTML { - w.Write(htmlScanReport) - } else { - if i.meta.OpenSCAP.Status == StatusError { - http.Error(w, fmt.Sprintf("OpenSCAP Error: %s", i.meta.OpenSCAP.ErrorMessage), - http.StatusInternalServerError) - } else { - http.Error(w, "OpenSCAP option was not chosen", http.StatusNotFound) - } - } - }) - - http.Handle(CONTENT_URL_PREFIX, &webdav.Handler{ - Prefix: CONTENT_URL_PREFIX, - FileSystem: webdav.Dir(servePath), - LockSystem: webdav.NewMemLS(), - }) - - return http.ListenAndServe(i.opts.Serve, nil) + if i.imageServer != nil { + return i.imageServer.ServeImage(&i.meta, + scanReport, htmlScanReport) } return nil } diff --git a/pkg/inspector/image-inspector_test.go b/pkg/inspector/image-inspector_test.go index 4e14974..5d820f9 100644 --- a/pkg/inspector/image-inspector_test.go +++ b/pkg/inspector/image-inspector_test.go @@ -2,13 +2,13 @@ package inspector import ( "fmt" - "io/ioutil" - "os" - "testing" - docker "github.com/fsouza/go-dockerclient" + iiapi "github.com/openshift/image-inspector/pkg/api" iicmd "github.com/openshift/image-inspector/pkg/cmd" "github.com/openshift/image-inspector/pkg/openscap" + "io/ioutil" + "os" + "testing" ) type FailMockScanner struct{} @@ -143,7 +143,7 @@ func TestGetAuthConfigs(t *testing.T) { } for k, v := range tests { - ii := &defaultImageInspector{*v.opts, InspectorMetadata{}} + ii := &defaultImageInspector{*v.opts, iiapi.InspectorMetadata{}, nil} auths, err := ii.getAuthConfigs() if !v.shouldFail { if err != nil { diff --git a/pkg/inspector/types.go b/pkg/inspector/types.go index 2f0535b..6cd19d5 100644 --- a/pkg/inspector/types.go +++ b/pkg/inspector/types.go @@ -1,47 +1 @@ package inspector - -import ( - docker "github.com/fsouza/go-dockerclient" - "time" -) - -// OpenSCAPStatus is the status of openscap scan -type OpenSCAPStatus string - -const ( - StatusNotRequested OpenSCAPStatus = "NotRequested" - StatusSuccess OpenSCAPStatus = "Success" - StatusError OpenSCAPStatus = "Error" -) - -type openSCAPMetadata struct { - Status OpenSCAPStatus // Status of the OpenSCAP scan report - ErrorMessage string // Error message from the openscap - ContentTimeStamp string // Timestamp for this data -} - -func (osm *openSCAPMetadata) SetError(err error) { - osm.Status = StatusError - osm.ErrorMessage = err.Error() - osm.ContentTimeStamp = string(time.Now().Format(time.RFC850)) -} - -// InspectorMetadata is the metadata type with information about image-inspector's operation -type InspectorMetadata struct { - docker.Image // Metadata about the inspected image - - OpenSCAP *openSCAPMetadata -} - -// NewInspectorMetadata returns a new InspectorMetadata out of *docker.Image -// The OpenSCAP status will be NotRequested -func NewInspectorMetadata(imageMetadata *docker.Image) *InspectorMetadata { - return &InspectorMetadata{ - Image: *imageMetadata, - OpenSCAP: &openSCAPMetadata{ - Status: StatusNotRequested, - ErrorMessage: "", - ContentTimeStamp: string(time.Now().Format(time.RFC850)), - }, - } -}