Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 97 additions & 51 deletions src/nodejs/hooks/contrast_security.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package hooks

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

Expand All @@ -15,6 +16,7 @@ type ContrastSecurityHook struct {

type ContrastSecurityCredentials struct {
ApiKey string
ApiToken string
OrgUuid string
ServiceKey string
ContrastUrl string //formerly Teamserver URL
Expand All @@ -29,6 +31,21 @@ func init() {
})
}

// shellQuote wraps a value in single quotes and escapes any embedded single
// quotes so the result is safe to embed in a shell export statement.
func shellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}

// writeExport returns a shell export line for the given variable, or an empty
// string when value is empty (so the line is simply omitted from the script).
func writeExport(name, value string) string {
if value == "" {
return ""
}
return "export " + name + "=" + shellQuote(value) + "\n"
}

func (h ContrastSecurityHook) AfterCompile(stager *libbuildpack.Stager) error {
h.Log.Debug("Contrast Security after compile hook")

Expand All @@ -41,10 +58,20 @@ func (h ContrastSecurityHook) AfterCompile(stager *libbuildpack.Stager) error {

h.Log.Info("Contrast Security credentials found. Configuring environment for [%s].", contrastSecurityCredentials.ContrastUrl)

var contrastSecurityScript = "export CONTRAST__API__API_KEY=" + contrastSecurityCredentials.ApiKey + "\n" +
"export CONTRAST__API__URL=" + contrastSecurityCredentials.ContrastUrl + "/Contrast/\n" +
"export CONTRAST__API__SERVICE_KEY=" + contrastSecurityCredentials.ServiceKey + "\n" +
"export CONTRAST__API__USER_NAME=" + contrastSecurityCredentials.Username + "\n"
url := ""
if contrastSecurityCredentials.ContrastUrl != "" {
url = strings.TrimRight(contrastSecurityCredentials.ContrastUrl, "/")
if !strings.HasSuffix(url, "/Contrast") {
url += "/Contrast"
}
}

contrastSecurityScript :=
writeExport("CONTRAST__API__URL", url) +
writeExport("CONTRAST__API__API_KEY", contrastSecurityCredentials.ApiKey) +
writeExport("CONTRAST__API__SERVICE_KEY", contrastSecurityCredentials.ServiceKey) +
writeExport("CONTRAST__API__USER_NAME", contrastSecurityCredentials.Username) +
writeExport("CONTRAST__API__TOKEN", contrastSecurityCredentials.ApiToken)

stager.WriteProfileD("contrast_security", contrastSecurityScript)

Expand All @@ -55,84 +82,103 @@ func (h ContrastSecurityHook) AfterCompile(stager *libbuildpack.Stager) error {

func getContrastCredentialString(credentials map[string]interface{}, key string) string {
if value, exists := credentials[key]; exists {
return value.(string)
if str, ok := value.(string); ok {
return str
}
}
return ""
}

func containsContrastService(key string, services interface{}, query string) bool {
var serviceName string
var serviceLabel string
var serviceTags []interface{}

// serviceMatchesContrast reports whether a single service entry (identified by
// its top-level VCAP_SERVICES category key plus its own fields) should be
// treated as a Contrast Security service. All type assertions use the comma-ok
// form so malformed entries are skipped rather than panicked on.
func serviceMatchesContrast(key string, svc map[string]interface{}, query string) bool {
if strings.Contains(key, query) {
return true
}
val := services.([]interface{})
for serviceIndex := range val {
service := val[serviceIndex].(map[string]interface{})
if v, ok := service["name"]; ok {
serviceName = v.(string)
}
if v, ok := service["label"]; ok {
serviceLabel = v.(string)
}
if strings.Contains(serviceName, query) || strings.Contains(serviceLabel, query) {
return true
}
if v, ok := service["tags"]; ok {
serviceTags = v.([]interface{})
}
for _, tagValue := range serviceTags {
if strings.Contains(tagValue.(string), query) {
if name, ok := svc["name"].(string); ok && strings.Contains(name, query) {
return true
}
if label, ok := svc["label"].(string); ok && strings.Contains(label, query) {
return true
}
if tags, ok := svc["tags"].([]interface{}); ok {
for _, t := range tags {
if tag, ok := t.(string); ok && strings.Contains(tag, query) {
return true
}
}
}
return false
}

// loadVcapServicesJSON returns the raw VCAP_SERVICES JSON from whichever
// delivery mechanism is active. VCAP_SERVICES_FILE_PATH (file-based CF mode)
// takes priority; VCAP_SERVICES env var is the fallback (standard CF mode).
func (h ContrastSecurityHook) loadVcapServicesJSON() (string, error) {
fp := os.Getenv("VCAP_SERVICES_FILE_PATH")
if fp != "" {
data, err := os.ReadFile(fp)
if err != nil {
return "", fmt.Errorf("could not read VCAP_SERVICES_FILE_PATH %q: %w", fp, err)
}
if os.Getenv("VCAP_SERVICES") != "" {
h.Log.Warning("Contrast Security: both VCAP_SERVICES and VCAP_SERVICES_FILE_PATH are set; using VCAP_SERVICES_FILE_PATH.")
}
return string(data), nil
}
return os.Getenv("VCAP_SERVICES"), nil
}

func (h ContrastSecurityHook) GetCredentialsFromEnvironment() (bool, ContrastSecurityCredentials) {

type rawVcapServicesJSONValue map[string]interface{}

var vcapServices rawVcapServicesJSONValue

vcapServicesEnvironment := os.Getenv("VCAP_SERVICES")
vcapServicesEnvironment, err := h.loadVcapServicesJSON()
if err != nil {
h.Log.Warning("Contrast Security: %s", err.Error())
return false, ContrastSecurityCredentials{}
}

if vcapServicesEnvironment == "" {
h.Log.Debug("Contrast Security could not find VCAP_SERVICES in the environment")
h.Log.Debug("Contrast Security could not find VCAP_SERVICES or VCAP_SERVICES_FILE_PATH in the environment")
return false, ContrastSecurityCredentials{}
}

err := json.Unmarshal([]byte(vcapServicesEnvironment), &vcapServices)
err = json.Unmarshal([]byte(vcapServicesEnvironment), &vcapServices)
if err != nil {
h.Log.Warning("Contrast Security could not parse VCAP_SERVICES")
return false, ContrastSecurityCredentials{}
}

for key, services := range vcapServices {
if containsContrastService(key, services, "contrast-security") {
for key, rawServices := range vcapServices {
services, ok := rawServices.([]interface{})
if !ok {
continue
}
for _, rawSvc := range services {
svc, ok := rawSvc.(map[string]interface{})
if !ok {
continue
}
if !serviceMatchesContrast(key, svc, "contrast-security") {
continue
}
credentials, ok := svc["credentials"].(map[string]interface{})
if !ok {
continue
}
h.Log.Debug("Contrast Security found credentials in VCAP_SERVICES")
val := services.([]interface{})
for serviceIndex := range val {
service := val[serviceIndex].(map[string]interface{})
if credentials, exists := service["credentials"].(map[string]interface{}); exists {
apiKey := getContrastCredentialString(credentials, "api_key")
orgUuid := getContrastCredentialString(credentials, "org_uuid")
serviceKey := getContrastCredentialString(credentials, "service_key")
contrastUrl := getContrastCredentialString(credentials, "teamserver_url")
username := getContrastCredentialString(credentials, "username")

contrastSecurityCredentials := ContrastSecurityCredentials{
ApiKey: apiKey,
OrgUuid: orgUuid,
ServiceKey: serviceKey,
ContrastUrl: contrastUrl,
Username: username,
}
return true, contrastSecurityCredentials
}
return true, ContrastSecurityCredentials{
ApiKey: getContrastCredentialString(credentials, "api_key"),
ApiToken: getContrastCredentialString(credentials, "api_token"),
OrgUuid: getContrastCredentialString(credentials, "org_uuid"),
ServiceKey: getContrastCredentialString(credentials, "service_key"),
ContrastUrl: getContrastCredentialString(credentials, "teamserver_url"),
Username: getContrastCredentialString(credentials, "username"),
}
}
}
Expand Down
Loading