From 68a4928018fb420dbfd7782611bee5d076a0d397 Mon Sep 17 00:00:00 2001 From: Ibrahim Abu Rmaileh Date: Thu, 19 Feb 2026 14:25:32 +0200 Subject: [PATCH 1/3] changes for enterprise --- cfg.yaml | 2 +- deploy/kubernetes/postee.yaml | 2 +- outputs/servicenow.go | 23 ++++++++++---------- router/builders.go | 2 +- router/initoutputs_test.go | 14 ++++++------ servicenow/insert_table.go | 10 ++++++--- servicenow/servicenow_base.go | 4 +--- ui/frontend/src/components/OutputDetails.vue | 15 ++----------- 8 files changed, 32 insertions(+), 40 deletions(-) diff --git a/cfg.yaml b/cfg.yaml index 846c5d5c..ebe1ff00 100644 --- a/cfg.yaml +++ b/cfg.yaml @@ -123,6 +123,6 @@ outputs: enable: false user: # Mandatory. E.g :johndoe@gmail.com" password: # Mandatory. Specify user API key - instance: # Mandatory. Name of ServiceN ow Instance + url: # Mandatory. ServiceNow instance URL (e.g. https://ven05031.service-now.com/ or https://fsadev.servicenowservices.com) board: # Specify the ServiceNow board name to open tickets on. Default is "incident" diff --git a/deploy/kubernetes/postee.yaml b/deploy/kubernetes/postee.yaml index 8977ce12..ea52d865 100644 --- a/deploy/kubernetes/postee.yaml +++ b/deploy/kubernetes/postee.yaml @@ -25,7 +25,7 @@ data: enable: false user: xxxxxxxxx password: xxxxxxxxxx - instance: xxxxxxxx + url: https://your-instance.service-now.com/ - type: slack name: my-slack enable: false diff --git a/outputs/servicenow.go b/outputs/servicenow.go index 560bcb14..4e72446c 100644 --- a/outputs/servicenow.go +++ b/outputs/servicenow.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "strconv" + "strings" "time" "github.com/pkg/errors" @@ -23,7 +24,7 @@ type ServiceNowOutput struct { Name string User string Password string - Instance string + Url string // ServiceNow instance URL (e.g. https://ven05031.service-now.com/ or https://fsadev.servicenowservices.com) Table string layoutProvider layout.LayoutProvider } @@ -38,13 +39,12 @@ func (sn *ServiceNowOutput) GetName() string { func (sn *ServiceNowOutput) CloneSettings() *data.OutputSettings { return &data.OutputSettings{ - Name: sn.Name, - User: sn.User, - //password - InstanceName: sn.Instance, - BoardName: sn.Table, - Enable: true, - Type: serviceNowType, + Name: sn.Name, + User: sn.User, + Url: sn.Url, + BoardName: sn.Table, + Enable: true, + Type: serviceNowType, } } @@ -52,7 +52,7 @@ func (sn *ServiceNowOutput) Init() error { sn.layoutProvider = new(formatting.HtmlProvider) log.Logger.Infof("Successfully initialized ServiceNow output %q", sn.Name) - log.Logger.Debugf("Your ServiceNow Table is %q on '%s.%s'", sn.Table, sn.Instance, servicenow.BaseServer) + log.Logger.Debugf("Your ServiceNow Table is %q at %q", sn.Table, sn.Url) return nil } @@ -90,13 +90,14 @@ func (sn *ServiceNowOutput) Send(content map[string]string) (data.OutputResponse return data.OutputResponse{}, errors.New("Error when trying to parse ServiceNow integration data") } - resp, err := servicenow.InsertRecordToTable(sn.User, sn.Password, sn.Instance, sn.Table, body) + resp, err := servicenow.InsertRecordToTable(sn.User, sn.Password, sn.Url, sn.Table, body) if err != nil { log.Logger.Error("ServiceNow Error: ", err) return data.OutputResponse{}, errors.New("Failed inserting record to the ServiceNow table") } - ticketLink := fmt.Sprintf("https://%s.service-now.com/nav_to.do?uri=%s.do?sys_id=%s", sn.Instance, sn.Table, resp.SysID) + baseURL := strings.TrimSuffix(sn.Url, "/") + ticketLink := fmt.Sprintf("%s/nav_to.do?uri=%s.do?sys_id=%s", baseURL, sn.Table, resp.SysID) log.Logger.Infof("Successfully sent a message via ServiceNow %q, ID %q, Link %q", sn.Name, resp.SysID, ticketLink) return data.OutputResponse{Key: resp.SysID, Url: ticketLink, Name: sn.Name}, nil } diff --git a/router/builders.go b/router/builders.go index ba1a403e..6eae93da 100644 --- a/router/builders.go +++ b/router/builders.go @@ -53,8 +53,8 @@ func buildServiceNow(sourceSettings *data.OutputSettings) *outputs.ServiceNowOut Name: sourceSettings.Name, User: sourceSettings.User, Password: sourceSettings.Password, + Url: sourceSettings.Url, Table: sourceSettings.BoardName, - Instance: sourceSettings.InstanceName, } if len(serviceNow.Table) == 0 { serviceNow.Table = ServiceNowTableDefault diff --git a/router/initoutputs_test.go b/router/initoutputs_test.go index 200018d0..7e2cd23c 100644 --- a/router/initoutputs_test.go +++ b/router/initoutputs_test.go @@ -178,17 +178,17 @@ func TestBuildAndInitOtpt(t *testing.T) { { "Simple ServiceNow output", data.OutputSettings{ - Name: "my-servicenow", - Type: "serviceNow", - User: "admin", - Password: "secret", - InstanceName: "dev108148", - BoardName: "incindent", + Name: "my-servicenow", + Type: "serviceNow", + User: "admin", + Password: "secret", + Url: "https://dev108148.service-now.com/", + BoardName: "incindent", }, map[string]interface{}{ "User": "admin", "Password": "secret", - "Instance": "dev108148", + "Url": "https://dev108148.service-now.com/", "Table": "incindent", }, false, diff --git a/servicenow/insert_table.go b/servicenow/insert_table.go index c168e900..16444b4f 100644 --- a/servicenow/insert_table.go +++ b/servicenow/insert_table.go @@ -7,13 +7,17 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" "github.com/aquasecurity/postee/v2/utils" ) -func InsertRecordToTable(user, password, instance, table string, content []byte) (*ServiceNowResponse, error) { - url := fmt.Sprintf("https://%s.%s%s%s%s", - instance, BaseServer, baseApiUrl, tableApi, table) +// InsertRecordToTable posts a record to the given ServiceNow table. +// instanceURL is the ServiceNow instance root URL (e.g. "https://ven05031.service-now.com/" or "https://fsadev.servicenowservices.com"), +// as provided by the customer; it is not constructed from instance name + baseServer. +func InsertRecordToTable(user, password, instanceURL, table string, content []byte) (*ServiceNowResponse, error) { + base := strings.TrimSuffix(instanceURL, "/") + url := base + "/" + baseApiPath + table r := bytes.NewReader(content) client := http.DefaultClient reg, err := http.NewRequest("POST", url, r) diff --git a/servicenow/servicenow_base.go b/servicenow/servicenow_base.go index e7dabf4f..1879e241 100644 --- a/servicenow/servicenow_base.go +++ b/servicenow/servicenow_base.go @@ -1,9 +1,7 @@ package servicenow_api const ( - BaseServer = "service-now.com/" - baseApiUrl = "api/now/" - tableApi = "table/" + baseApiPath = "api/now/table/" ) type ServiceNowData struct { diff --git a/ui/frontend/src/components/OutputDetails.vue b/ui/frontend/src/components/OutputDetails.vue index 70162e60..2ff8c487 100644 --- a/ui/frontend/src/components/OutputDetails.vue +++ b/ui/frontend/src/components/OutputDetails.vue @@ -362,19 +362,7 @@ -
-
- -
+
Date: Sun, 22 Feb 2026 15:14:18 +0200 Subject: [PATCH 2/3] support backward compitability - legacy code --- outputs/servicenow.go | 32 ++++++++++++++------ router/builders.go | 1 + router/initoutputs_test.go | 21 ++++++++++++- servicenow/insert_table.go | 21 +++++++++---- servicenow/servicenow_base.go | 2 ++ ui/frontend/src/components/OutputDetails.vue | 24 +++++++++++++-- 6 files changed, 81 insertions(+), 20 deletions(-) diff --git a/outputs/servicenow.go b/outputs/servicenow.go index 4e72446c..11082632 100644 --- a/outputs/servicenow.go +++ b/outputs/servicenow.go @@ -24,7 +24,8 @@ type ServiceNowOutput struct { Name string User string Password string - Url string // ServiceNow instance URL (e.g. https://ven05031.service-now.com/ or https://fsadev.servicenowservices.com) + Url string // ServiceNow instance URL (new behaviour). If set, used as-is; otherwise Instance + BaseServer is used (legacy). + Instance string // Legacy: instance name (e.g. dev12345) for https://.service-now.com/ Table string layoutProvider layout.LayoutProvider } @@ -39,12 +40,13 @@ func (sn *ServiceNowOutput) GetName() string { func (sn *ServiceNowOutput) CloneSettings() *data.OutputSettings { return &data.OutputSettings{ - Name: sn.Name, - User: sn.User, - Url: sn.Url, - BoardName: sn.Table, - Enable: true, - Type: serviceNowType, + Name: sn.Name, + User: sn.User, + Url: sn.Url, + InstanceName: sn.Instance, + BoardName: sn.Table, + Enable: true, + Type: serviceNowType, } } @@ -52,7 +54,11 @@ func (sn *ServiceNowOutput) Init() error { sn.layoutProvider = new(formatting.HtmlProvider) log.Logger.Infof("Successfully initialized ServiceNow output %q", sn.Name) - log.Logger.Debugf("Your ServiceNow Table is %q at %q", sn.Table, sn.Url) + if sn.Url != "" { + log.Logger.Debugf("Your ServiceNow Table is %q at %q (instance URL)", sn.Table, sn.Url) + } else { + log.Logger.Debugf("Your ServiceNow Table is %q on %s.%s (legacy)", sn.Table, sn.Instance, servicenow.BaseServer) + } return nil } @@ -90,13 +96,19 @@ func (sn *ServiceNowOutput) Send(content map[string]string) (data.OutputResponse return data.OutputResponse{}, errors.New("Error when trying to parse ServiceNow integration data") } - resp, err := servicenow.InsertRecordToTable(sn.User, sn.Password, sn.Url, sn.Table, body) + resp, err := servicenow.InsertRecordToTable(sn.User, sn.Password, sn.Url, sn.Instance, sn.Table, body) if err != nil { log.Logger.Error("ServiceNow Error: ", err) return data.OutputResponse{}, errors.New("Failed inserting record to the ServiceNow table") } - baseURL := strings.TrimSuffix(sn.Url, "/") + var baseURL string + if sn.Url != "" { + baseURL = strings.TrimSuffix(sn.Url, "/") + } else { + baseURL = fmt.Sprintf("https://%s.%s", sn.Instance, servicenow.BaseServer) + baseURL = strings.TrimSuffix(baseURL, "/") + } ticketLink := fmt.Sprintf("%s/nav_to.do?uri=%s.do?sys_id=%s", baseURL, sn.Table, resp.SysID) log.Logger.Infof("Successfully sent a message via ServiceNow %q, ID %q, Link %q", sn.Name, resp.SysID, ticketLink) return data.OutputResponse{Key: resp.SysID, Url: ticketLink, Name: sn.Name}, nil diff --git a/router/builders.go b/router/builders.go index 6eae93da..dfa7df56 100644 --- a/router/builders.go +++ b/router/builders.go @@ -54,6 +54,7 @@ func buildServiceNow(sourceSettings *data.OutputSettings) *outputs.ServiceNowOut User: sourceSettings.User, Password: sourceSettings.Password, Url: sourceSettings.Url, + Instance: sourceSettings.InstanceName, Table: sourceSettings.BoardName, } if len(serviceNow.Table) == 0 { diff --git a/router/initoutputs_test.go b/router/initoutputs_test.go index 7e2cd23c..8e60b270 100644 --- a/router/initoutputs_test.go +++ b/router/initoutputs_test.go @@ -176,7 +176,7 @@ func TestBuildAndInitOtpt(t *testing.T) { "*outputs.WebhookOutput", }, { - "Simple ServiceNow output", + "Simple ServiceNow output (url)", data.OutputSettings{ Name: "my-servicenow", Type: "serviceNow", @@ -194,6 +194,25 @@ func TestBuildAndInitOtpt(t *testing.T) { false, "*outputs.ServiceNowOutput", }, + { + "ServiceNow output (legacy, instance only)", + data.OutputSettings{ + Name: "my-servicenow-legacy", + Type: "serviceNow", + User: "admin", + Password: "secret", + InstanceName: "dev108148", + BoardName: "incident", + }, + map[string]interface{}{ + "User": "admin", + "Password": "secret", + "Instance": "dev108148", + "Table": "incident", + }, + false, + "*outputs.ServiceNowOutput", + }, { "Simple Teams output", data.OutputSettings{ diff --git a/servicenow/insert_table.go b/servicenow/insert_table.go index 16444b4f..7a88773c 100644 --- a/servicenow/insert_table.go +++ b/servicenow/insert_table.go @@ -13,14 +13,23 @@ import ( ) // InsertRecordToTable posts a record to the given ServiceNow table. -// instanceURL is the ServiceNow instance root URL (e.g. "https://ven05031.service-now.com/" or "https://fsadev.servicenowservices.com"), -// as provided by the customer; it is not constructed from instance name + baseServer. -func InsertRecordToTable(user, password, instanceURL, table string, content []byte) (*ServiceNowResponse, error) { - base := strings.TrimSuffix(instanceURL, "/") - url := base + "/" + baseApiPath + table +// If instanceURL is non-empty, it is used as the instance root URL (new behaviour; e.g. https://ven05031.service-now.com/ or https://fsadev.servicenowservices.com). +// If instanceURL is empty, the URL is built from instance + BaseServer (legacy behaviour: https://.service-now.com/). +// At least one of instanceURL or instance must be non-empty. +func InsertRecordToTable(user, password, instanceURL, instance, table string, content []byte) (*ServiceNowResponse, error) { + if instanceURL == "" && instance == "" { + return nil, fmt.Errorf("InsertRecordToTable: either url (instance URL) or instance (legacy) must be set") + } + var tableURL string + if instanceURL != "" { + base := strings.TrimSuffix(instanceURL, "/") + tableURL = base + "/" + baseApiPath + table + } else { + tableURL = fmt.Sprintf("https://%s.%s%s%s", instance, BaseServer, baseApiPath, table) + } r := bytes.NewReader(content) client := http.DefaultClient - reg, err := http.NewRequest("POST", url, r) + reg, err := http.NewRequest("POST", tableURL, r) if err != nil { return nil, err } diff --git a/servicenow/servicenow_base.go b/servicenow/servicenow_base.go index 1879e241..80696a5d 100644 --- a/servicenow/servicenow_base.go +++ b/servicenow/servicenow_base.go @@ -1,6 +1,8 @@ package servicenow_api const ( + // BaseServer is used for legacy config when url is not provided (e.g. instance name + BaseServer). + BaseServer = "service-now.com/" baseApiPath = "api/now/table/" ) diff --git a/ui/frontend/src/components/OutputDetails.vue b/ui/frontend/src/components/OutputDetails.vue index 2ff8c487..6eb3251e 100644 --- a/ui/frontend/src/components/OutputDetails.vue +++ b/ui/frontend/src/components/OutputDetails.vue @@ -107,7 +107,7 @@ :description="getUrlDescription" :show="showUrl" :inputHandler="updateField" - :validator="v([url, required])" + :validator="getUrlValidator" /> @@ -362,7 +362,18 @@ -
+
+
+ +
Date: Sun, 1 Mar 2026 13:14:50 +0200 Subject: [PATCH 3/3] adding url format validation to new url-based flow --- servicenow/insert_table.go | 6 ++++++ servicenow/urlvalidate.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 servicenow/urlvalidate.go diff --git a/servicenow/insert_table.go b/servicenow/insert_table.go index 7a88773c..c2df611d 100644 --- a/servicenow/insert_table.go +++ b/servicenow/insert_table.go @@ -22,6 +22,12 @@ func InsertRecordToTable(user, password, instanceURL, instance, table string, co } var tableURL string if instanceURL != "" { + if !IsValidURL(instanceURL) { + return nil, fmt.Errorf("InsertRecordToTable: invalid ServiceNow instance URL format (must be http(s) with host)") + } + if !IsUrlNotLocalhost(instanceURL) { + return nil, fmt.Errorf("InsertRecordToTable: ServiceNow instance URL must not be localhost") + } base := strings.TrimSuffix(instanceURL, "/") tableURL = base + "/" + baseApiPath + table } else { diff --git a/servicenow/urlvalidate.go b/servicenow/urlvalidate.go new file mode 100644 index 00000000..f41c864c --- /dev/null +++ b/servicenow/urlvalidate.go @@ -0,0 +1,30 @@ +package servicenow_api + +import ( + "net/url" + "regexp" + "strings" +) + +// IsValidURL returns true when the URL is valid: parseable, scheme "http" or "https", and non-empty host. +func IsValidURL(URL string) bool { + u, err := url.ParseRequestURI(URL) + if err != nil { + return false + } + if u.Scheme != "http" && u.Scheme != "https" { + return false + } + if u.Host == "" { + return false + } + return true +} + +var localhostURLPattern = regexp.MustCompile(`^http://localhost|^https://localhost|^localhost|127\.0\.0\.1|\[::\]`) + +// IsUrlNotLocalhost returns true when the URL does not refer to localhost (or 127.0.0.1, [::]). +// Used to reject localhost URLs for ServiceNow instance URL. +func IsUrlNotLocalhost(URL string) bool { + return !localhostURLPattern.MatchString(strings.ToLower(URL)) +}