Skip to content
Merged
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
2 changes: 1 addition & 1 deletion cfg.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

2 changes: 1 addition & 1 deletion deploy/kubernetes/postee.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 20 additions & 7 deletions outputs/servicenow.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"

"github.com/pkg/errors"
Expand All @@ -23,7 +24,8 @@ type ServiceNowOutput struct {
Name string
User string
Password string
Instance string
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://<instance>.service-now.com/
Table string
layoutProvider layout.LayoutProvider
}
Expand All @@ -38,9 +40,9 @@ func (sn *ServiceNowOutput) GetName() string {

func (sn *ServiceNowOutput) CloneSettings() *data.OutputSettings {
return &data.OutputSettings{
Name: sn.Name,
User: sn.User,
//password
Name: sn.Name,
User: sn.User,
Url: sn.Url,
InstanceName: sn.Instance,
BoardName: sn.Table,
Enable: true,
Expand All @@ -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 on '%s.%s'", sn.Table, sn.Instance, servicenow.BaseServer)
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
}

Expand Down Expand Up @@ -90,13 +96,20 @@ 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.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")
}

ticketLink := fmt.Sprintf("https://%s.service-now.com/nav_to.do?uri=%s.do?sys_id=%s", sn.Instance, sn.Table, resp.SysID)
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
}
Expand Down
3 changes: 2 additions & 1 deletion router/builders.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ func buildServiceNow(sourceSettings *data.OutputSettings) *outputs.ServiceNowOut
Name: sourceSettings.Name,
User: sourceSettings.User,
Password: sourceSettings.Password,
Table: sourceSettings.BoardName,
Url: sourceSettings.Url,
Instance: sourceSettings.InstanceName,
Table: sourceSettings.BoardName,
}
if len(serviceNow.Table) == 0 {
serviceNow.Table = ServiceNowTableDefault
Expand Down
27 changes: 23 additions & 4 deletions router/initoutputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,39 @@ func TestBuildAndInitOtpt(t *testing.T) {
"*outputs.WebhookOutput",
},
{
"Simple ServiceNow output",
"Simple ServiceNow output (url)",
data.OutputSettings{
Name: "my-servicenow",
Name: "my-servicenow",
Type: "serviceNow",
User: "admin",
Password: "secret",
Url: "https://dev108148.service-now.com/",
BoardName: "incindent",
},
map[string]interface{}{
"User": "admin",
"Password": "secret",
"Url": "https://dev108148.service-now.com/",
"Table": "incindent",
},
false,
"*outputs.ServiceNowOutput",
},
{
"ServiceNow output (legacy, instance only)",
data.OutputSettings{
Name: "my-servicenow-legacy",
Type: "serviceNow",
User: "admin",
Password: "secret",
InstanceName: "dev108148",
BoardName: "incindent",
BoardName: "incident",
},
map[string]interface{}{
"User": "admin",
"Password": "secret",
"Instance": "dev108148",
"Table": "incindent",
"Table": "incident",
},
false,
"*outputs.ServiceNowOutput",
Expand Down
27 changes: 23 additions & 4 deletions servicenow/insert_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,35 @@ 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.
// 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://<instance>.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 != "" {
Comment thread
Ibrahim-Abu-Rmaileh marked this conversation as resolved.
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 {
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
}
Expand Down
6 changes: 3 additions & 3 deletions servicenow/servicenow_base.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package servicenow_api

const (
BaseServer = "service-now.com/"
baseApiUrl = "api/now/"
tableApi = "table/"
// BaseServer is used for legacy config when url is not provided (e.g. instance name + BaseServer).
BaseServer = "service-now.com/"
baseApiPath = "api/now/table/"
)

type ServiceNowData struct {
Expand Down
30 changes: 30 additions & 0 deletions servicenow/urlvalidate.go
Original file line number Diff line number Diff line change
@@ -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))
}
15 changes: 11 additions & 4 deletions ui/frontend/src/components/OutputDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
:description="getUrlDescription"
:show="showUrl"
:inputHandler="updateField"
:validator="v([url, required])"
:validator="getUrlValidator"
/>

<!-- email custom properties start -->
Expand Down Expand Up @@ -366,13 +366,12 @@
<div class="col">
<PropertyField
id="instance"
label="Instance"
label="Instance (legacy)"
:value="formValues.instance"
description="Mandatory. Name of ServiceNow or Instance"
description="Legacy: instance name (e.g. dev12345) if URL is not set. Used for https://&lt;instance&gt;.service-now.com"
:errorMsg="errors['instance']"
:show="isServiceNow"
Comment thread
Ibrahim-Abu-Rmaileh marked this conversation as resolved.
:inputHandler="updateField"
:validator="v(required)"
/>
</div>
<div class="col">
Expand Down Expand Up @@ -434,6 +433,7 @@ const urlDescriptionByType = {
teams: "Webhook's url",
jira: 'Mandatory. E.g "https://johndoe.atlassian.net"',
slack: "",
serviceNow: "If set, used as instance URL (recommended). Otherwise Instance name is used (legacy). E.g. https://ven05031.service-now.com/ or https://fsadev.servicenowservices.com",
};
const typesWithCredentials = ["serviceNow", "email"]; //TODO add description strings

Expand Down Expand Up @@ -477,6 +477,13 @@ export default {
getUrlDescription() {
return urlDescriptionByType[this.outputType];
},
getUrlValidator() {
// ServiceNow: url is optional (legacy uses instance only)
if (this.outputType === "serviceNow") {
return this.v([this.url]);
}
return this.v([this.url, this.required]);
},
isServiceNow() {
return this.outputType === "serviceNow";
},
Expand Down
Loading