Skip to content

Commit

Permalink
refactor validate handler to move specific functionality into webhook…
Browse files Browse the repository at this point in the history
… package
  • Loading branch information
Yusuf Kanchwala committed Mar 30, 2021
1 parent 8a95043 commit 0686262
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 102 deletions.
7 changes: 1 addition & 6 deletions pkg/http-server/webhook-scan-logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) {
logsData = append(logsData, webhookDisplayedIndexScanLog{
CreatedAt: log.CreatedAt,
Status: g.getLogStatus(log),
LogURL: g.getLogPath(r.Host, log.UID),
LogURL: logger.GetLogURL(r.Host, log.UID),
Reasoning: g.getLogReasoning(log),
Request: g.getLogRequest(log),
})
Expand Down Expand Up @@ -143,11 +143,6 @@ func (g *APIHandler) getLogByUID(w http.ResponseWriter, r *http.Request) {
t.Execute(w, displayedScanLog)
}

func (g *APIHandler) getLogPath(host, logUID string) string {
// Use this as the link to show the a specific log
return fmt.Sprintf("https://%v/k8s/webhooks/logs/%v", host, logUID)
}

func (g *APIHandler) getLogStatus(log dblogs.WebhookScanLog) string {
// Calculate a log status:
// 1. !Allowed -> Rejected
Expand Down
91 changes: 5 additions & 86 deletions pkg/http-server/webhook-scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,18 @@ import (
"fmt"
"io/ioutil"
"net/http"
"time"

admissionWebhook "github.com/accurics/terrascan/pkg/k8s/admission-webhook"
"github.com/accurics/terrascan/pkg/k8s/dblogs"
"github.com/accurics/terrascan/pkg/results"
"github.com/accurics/terrascan/pkg/runtime"
"github.com/gorilla/mux"
"go.uber.org/zap"

v1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// validateK8SWebhook handles the incoming validating admission webhook from kubernetes API server
func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) {

var (
currentTime = time.Now()
params = mux.Vars(r)
apiKey = params["apiKey"]
validatingWebhook = admissionWebhook.NewValidatingWebhook(g.configFile)
Expand Down Expand Up @@ -79,56 +73,18 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request)
}

// process the admission review request
output, allowed, denyViolations, err := validatingWebhook.ProcessWebhook(requestedAdmissionReview)
if err != nil {
if err == admissionWebhook.ErrEmptyAdmissionReview {
g.sendResponseAdmissionReview(w, requestedAdmissionReview, true, output, "")
return
}
apiErrorResponse(w, err.Error(), http.StatusInternalServerError)
return
}

logPath := g.getLogPath(r.Host, string(requestedAdmissionReview.Request.UID))

// Log the request in the DB
err = g.logWebhook(output, string(requestedAdmissionReview.Request.UID), body, denyViolations, currentTime, allowed)
if err != nil {
admissionResponse, err := validatingWebhook.ProcessWebhook(requestedAdmissionReview, r.Host)
if err != nil && err != admissionWebhook.ErrEmptyAdmissionReview {
apiErrorResponse(w, err.Error(), http.StatusInternalServerError)
return
}

// Send the correct response according to the result
g.sendResponseAdmissionReview(w, requestedAdmissionReview, allowed, output, logPath)
g.sendResponseAdmissionReview(w, admissionResponse)
}

func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter,
requestedAdmissionReview v1.AdmissionReview,
allowed bool,
output runtime.Output,
logPath string) {
responseAdmissionReview := &v1.AdmissionReview{}
responseAdmissionReview.SetGroupVersionKind(requestedAdmissionReview.GroupVersionKind())

responseAdmissionReview.Response = &v1.AdmissionResponse{
UID: requestedAdmissionReview.Request.UID,
Allowed: allowed,
}

if output.Violations.ViolationStore != nil {
// Means we ran the engines and we have results
if allowed {
if len(output.Violations.ViolationStore.Violations) > 0 {
// In case there are no denial violations, just return the log URL as a warning
responseAdmissionReview.Response.Warnings = []string{logPath}
}
} else {
// In case the request was denied, return 403 and the log URL as an error message
responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403}
}
}

respBytes, err := json.Marshal(responseAdmissionReview)
func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, admissionResponse *v1.AdmissionReview) {
respBytes, err := json.Marshal(admissionResponse)
if err != nil {
msg := fmt.Sprintf("failed to serialize admission review response: %v", err)
zap.S().Error(msg)
Expand All @@ -138,40 +94,3 @@ func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter,
zap.S().Debugf("Response result: %+v", string(respBytes))
apiResponse(w, string(respBytes), http.StatusOK)
}

func (g *APIHandler) logWebhook(output runtime.Output,
uid string,
bytesAdmissionReview []byte,
denyViolations []results.Violation,
currentTime time.Time,
allowed bool) error {
var deniedViolationsEncoded string

if len(denyViolations) < 1 {
deniedViolationsEncoded = ""
} else {
d, _ := json.Marshal(denyViolations)
deniedViolationsEncoded = string(d)
}

encodedViolationsSummary, _ := json.Marshal(output.Violations.ViolationStore)

logger := dblogs.WebhookScanLogger{
Test: g.test,
}

err := logger.Log(dblogs.WebhookScanLog{
UID: uid,
Request: string(bytesAdmissionReview),
Allowed: allowed,
DeniableViolations: deniedViolationsEncoded,
ViolationsSummary: string(encodedViolationsSummary),
CreatedAt: currentTime,
})
if err != nil {
zap.S().Error("error logging scan result: '%v'", err)
return err
}

return nil
}
5 changes: 2 additions & 3 deletions pkg/k8s/admission-webhook/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@
package admissionwebhook

import (
"github.com/accurics/terrascan/pkg/results"
"github.com/accurics/terrascan/pkg/runtime"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/admission/v1"
)

// AdmissionWebhook interface needs to be implemented by all k8s admission
Expand All @@ -35,5 +34,5 @@ type AdmissionWebhook interface {

// ProcessWebhook processes the incoming AdmissionReview and creates
// a AdmissionResponse
ProcessWebhook(review admissionv1.AdmissionReview) (runtime.Output, bool, []results.Violation, error)
ProcessWebhook(review admissionv1.AdmissionReview, serverURL string) (*v1.AdmissionReview, error)
}
116 changes: 109 additions & 7 deletions pkg/k8s/admission-webhook/validating-webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@
package admissionwebhook

import (
"encoding/json"
"fmt"
"os"
"time"

"github.com/accurics/terrascan/pkg/config"
"github.com/accurics/terrascan/pkg/k8s/dblogs"
"github.com/accurics/terrascan/pkg/results"
"github.com/accurics/terrascan/pkg/runtime"
"github.com/accurics/terrascan/pkg/utils"
"go.uber.org/zap"

admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtimeK8s "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
Expand All @@ -34,12 +40,17 @@ import (
// the kubernetes API server and decides whether the admission request from
// the kubernetes client should be allowed or not
type ValidatingWebhook struct {
configFile string
configFile string
requestBody []byte
dblogger *dblogs.WebhookScanLogger
}

// NewValidatingWebhook returns a new, empty ValidatingWebhook struct
func NewValidatingWebhook(configFile string) AdmissionWebhook {
return ValidatingWebhook{configFile: configFile}
return ValidatingWebhook{
configFile: configFile,
dblogger: dblogs.NewWebhookScanLogger(),
}
}

var (
Expand Down Expand Up @@ -91,6 +102,7 @@ func (w ValidatingWebhook) DecodeAdmissionReviewRequest(requestBody []byte) (adm
deserializer = codecs.UniversalDeserializer()
requestedAdmissionReview admissionv1.AdmissionReview
)
w.requestBody = requestBody
admissionv1.AddToScheme(scheme)

// decode incoming admission request
Expand All @@ -106,12 +118,19 @@ func (w ValidatingWebhook) DecodeAdmissionReviewRequest(requestBody []byte) (adm

// ProcessWebhook processes the incoming AdmissionReview and creates
// a response
func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (output runtime.Output, allowed bool, denyViolations []results.Violation, err error) {
func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview, serverURL string) (*v1.AdmissionReview, error) {

var (
output runtime.Output
denyViolations []results.Violation
logURL = w.dblogger.GetLogURL(serverURL, string(review.Request.UID))
allowed = false
)

// In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check
if len(review.Request.Object.Raw) < 1 {
zap.S().Info(ErrEmptyAdmissionReview, zap.Any("admission review object", review))
return output, true, denyViolations, ErrEmptyAdmissionReview
return w.createResponseAdmissionReview(review, true, output, logURL), ErrEmptyAdmissionReview
}

// Save the object into a temp file for the policy engines
Expand All @@ -120,22 +139,34 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (o
if err != nil {
msg := "failed to create temp file for validating admission review request"
zap.S().Error(msg, zap.Error(err))
return output, true, denyViolations, fmt.Errorf("%s; error: %w", msg, err)
return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf("%s; error: %w", msg, err)
}

// Run the policy engines
output, err = w.scanK8sFile(tempFile.Name())
if err != nil {
msg := "failed to evaluate terrascan policies"
zap.S().Errorf(msg, zap.Error(err))
return output, allowed, denyViolations, fmt.Errorf("%s; error: %w", msg, err)
return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf("%s; error: %w", msg, err)
}

// Calculate if there are anydeny violations
denyViolations, err = w.getDenyViolations(output)
if err != nil {
msg := "failed to figure out denied violations"
zap.S().Errorf(msg, zap.Error(err))
return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf("%s; error: %w", msg, err)
}
allowed = len(denyViolations) < 1

return output, allowed, denyViolations, nil
// Log the request in the DB
err = w.logWebhook(output, string(review.Request.UID), denyViolations, allowed)
if err != nil {
msg := "failed to log validating admission review request into database"
zap.S().Error(msg, zap.Error(err))
}

return w.createResponseAdmissionReview(review, allowed, output, logURL), nil
}

func (w ValidatingWebhook) scanK8sFile(filePath string) (runtime.Output, error) {
Expand Down Expand Up @@ -193,6 +224,77 @@ func (w ValidatingWebhook) getDeniedViolations(violations results.ViolationStore
return denyViolations
}

func (w ValidatingWebhook) logWebhook(output runtime.Output,
uid string,
denyViolations []results.Violation,
allowed bool) error {

var (
currentTime = time.Now()
deniedViolationsEncoded string
)

// encode denied violations into a string
if len(denyViolations) < 1 {
deniedViolationsEncoded = ""
} else {
d, _ := json.Marshal(denyViolations)
deniedViolationsEncoded = string(d)
}

encodedViolationsSummary, _ := json.Marshal(output.Violations.ViolationStore)

// insert the webhook log into db
err := w.dblogger.Log(dblogs.WebhookScanLog{
UID: uid,
Request: string(w.requestBody),
Allowed: allowed,
DeniableViolations: deniedViolationsEncoded,
ViolationsSummary: string(encodedViolationsSummary),
CreatedAt: currentTime,
})
if err != nil {
zap.S().Error("error logging scan result: '%v'", err)
return err
}

return nil
}

// createAdmissionResponse creates a admission review response which is sent
// to calling kubernetes API server
func (w ValidatingWebhook) createResponseAdmissionReview(
requestedAdmissionReview v1.AdmissionReview,
allowed bool,
output runtime.Output,
logPath string) *v1.AdmissionReview {

// create an admission review request to be sent as response
responseAdmissionReview := &v1.AdmissionReview{}
responseAdmissionReview.SetGroupVersionKind(requestedAdmissionReview.GroupVersionKind())

// populate admission response
responseAdmissionReview.Response = &v1.AdmissionResponse{
UID: requestedAdmissionReview.Request.UID,
Allowed: allowed,
}

if output.Violations.ViolationStore != nil {
// Means we ran the engines and we have results
if allowed {
if len(output.Violations.ViolationStore.Violations) > 0 {
// In case there are no denial violations, just return the log URL as a warning
responseAdmissionReview.Response.Warnings = []string{logPath}
}
} else {
// In case the request was denied, return 403 and the log URL as an error message
responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403}
}
}

return responseAdmissionReview
}

type webhookDenyRuleMatcher struct {
}

Expand Down
7 changes: 7 additions & 0 deletions pkg/k8s/dblogs/webhook-scan-logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package dblogs

import (
"database/sql"
"fmt"
"os"
"time"

Expand Down Expand Up @@ -163,6 +164,12 @@ func (g *WebhookScanLogger) FetchLogByID(logUID string) (*WebhookScanLog, error)
return &WebhookScanLog{}, nil
}

// GetLogURL returns a url to the UI page for reviewing the validating admission request log
func (g *WebhookScanLogger) GetLogURL(host, logUID string) string {
// Use this as the link to show the a specific log
return fmt.Sprintf("https://%v/k8s/webhooks/logs/%v", host, logUID)
}

func (g *WebhookScanLogger) initDBIfNeeded() error {
// Check where the SQL file exists. If it does do nothing. Otherwise, create the DB file and the Logs table.
if _, err := os.Stat(g.dbFilePath()); os.IsNotExist(err) {
Expand Down

0 comments on commit 0686262

Please sign in to comment.