Files
shuffle-cracked/backend/go-app/shuffle-shared/cloudSync.go
T
Marat Kharitonov 4f3f07d4dd Crack: bypass license check - force all limits unlimited
- Vendor shuffle-shared v1.2.51 as backend/go-app/shuffle-shared
- Add replace directive in go.mod to use the local moduled copy
- In HandleCheckLicense, force org.Licensed=true and set every
  SyncFeatures limit to 1e9, skipping all license-key logic
- Update Dockerfile to ADD the local shuffle-shared before go build
- Verified: backend image builds successfully via docker
2026-08-12 03:14:24 +03:00

3655 lines
113 KiB
Go

package shuffle
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"strconv"
"sync"
"encoding/base64"
//"github.com/algolia/algoliasearch-client-go/v3/algolia/opt"
"github.com/algolia/algoliasearch-client-go/v3/algolia/search"
"github.com/frikky/schemaless"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
//"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
"github.com/go-git/go-git/v5/plumbing/transport"
gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
uuid "github.com/satori/go.uuid"
)
func executeCloudAction(action CloudSyncJob, apikey string) error {
data, err := json.Marshal(action)
if err != nil {
log.Printf("Failed cloud webhook action marshalling: %s", err)
return err
}
client := &http.Client{}
syncUrl := fmt.Sprintf("https://shuffler.io/api/v1/cloud/sync/handle_action")
req, err := http.NewRequest(
"POST",
syncUrl,
bytes.NewBuffer(data),
)
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey))
newresp, err := client.Do(req)
if err != nil {
return err
}
defer newresp.Body.Close()
respBody, err := ioutil.ReadAll(newresp.Body)
if err != nil {
return err
}
type Result struct {
Success bool `json:"success"`
Reason string `json:"reason"`
}
//log.Printf("Data: %s", string(respBody))
responseData := Result{}
err = json.Unmarshal(respBody, &responseData)
if err != nil {
return err
}
if !responseData.Success {
return errors.New(fmt.Sprintf("Cloud error from Shuffler: %s", responseData.Reason))
}
return nil
}
func HandleAlgoliaAppSearch(ctx context.Context, appname string) (AlgoliaSearchApp, error) {
cacheTimer := int32(300)
normalizedAppName := strings.TrimSpace(strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(appname, "_", " "), " ", "_")))
cacheKey := fmt.Sprintf("appsearch_%s", normalizedAppName)
cache, err := GetCache(ctx, cacheKey)
if err == nil {
if cacheData, ok := cache.([]byte); ok {
var cachedApp AlgoliaSearchApp
err = json.Unmarshal(cacheData, &cachedApp)
if err == nil {
return cachedApp, nil
}
log.Printf("[ERROR] Failed unmarshalling cached app search data in Handle algolia app search: %s", err)
}
}
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
// Fallback to default Algolia keys
if len(algoliaSecret) == 0 {
algoliaClient = "JNSS5CFDZZ"
algoliaSecret = os.Getenv("ALGOLIA_PUBLICKEY")
}
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[ERROR] ALGOLIA_CLIENT and ALGOLIA_SECRET/ALGOLIA_SECRET not defined (app discovery)")
return AlgoliaSearchApp{}, errors.New("Algolia keys not defined")
}
returnApp := AlgoliaSearchApp{}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("appsearch")
appname = strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(appname, "_", " ", -1), "-", " ", -1)))
res, err := algoliaIndex.Search(appname)
if err != nil {
log.Printf("[ERROR] Failed searching Algolia (%s): %s", appname, err)
appData, err := json.Marshal(returnApp)
if err == nil {
SetCache(ctx, cacheKey, appData, cacheTimer)
} else {
log.Printf("[ERROR] Failed to marshal Algolia result in handle aloglia search (3): %s", err)
}
return returnApp, err
}
var newRecords []AlgoliaSearchApp
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia: %s", err)
appData, err := json.Marshal(returnApp)
if err == nil {
SetCache(ctx, cacheKey, appData, cacheTimer)
} else {
log.Printf("[ERROR] Failed to marshal Algolia result in handle aloglia search (4): %s", err)
}
return returnApp, err
}
if debug {
log.Printf("[DEBUG] Got %d hits matching appname '%s'", len(newRecords), appname)
}
for _, newRecord := range newRecords {
newApp := strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(newRecord.Name, "_", " ", -1), "-", " ", -1)))
if newApp == appname || newRecord.ObjectID == appname {
//return newRecord.ObjectID, nil
appData, err := json.Marshal(newRecord)
if err == nil {
SetCache(ctx, cacheKey, appData, cacheTimer)
} else {
log.Printf("[ERROR] Failed to marshal Algolia result in handle aloglia search (5): %s", err)
}
return newRecord, nil
}
}
// Second try with contains
for _, newRecord := range newRecords {
newApp := strings.TrimSpace(strings.ToLower(strings.Replace(newRecord.Name, "_", " ", -1)))
if strings.Contains(newApp, appname) {
appData, err := json.Marshal(newRecord)
if err == nil {
SetCache(ctx, cacheKey, appData, cacheTimer)
} else {
log.Printf("[ERROR] Failed to marshal Algolia result in handle aloglia search (6): %s", err)
}
return newRecord, nil
}
}
appData, err := json.Marshal(returnApp)
if err == nil {
SetCache(ctx, cacheKey, appData, cacheTimer)
} else {
log.Printf("[ERROR] Failed to marshal Algolia result in handle aloglia search (7): %s", err)
}
return returnApp, nil
}
func HandleAlgoliaWorkflowSearchByApp(ctx context.Context, appname string) ([]AlgoliaSearchWorkflow, error) {
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined")
return []AlgoliaSearchWorkflow{}, errors.New("Algolia keys not defined")
}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("workflows")
appSearch := fmt.Sprintf("%s", appname)
res, err := algoliaIndex.Search(appSearch)
if err != nil {
log.Printf("[WARNING] Failed app searching Algolia for creators: %s", err)
return []AlgoliaSearchWorkflow{}, err
}
var newRecords []AlgoliaSearchWorkflow
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia with app creators: %s", err)
return []AlgoliaSearchWorkflow{}, err
}
//log.Printf("[INFO] Algolia hits for %s: %d", appSearch, len(newRecords))
allRecords := []AlgoliaSearchWorkflow{}
for _, newRecord := range newRecords {
allRecords = append(allRecords, newRecord)
}
return allRecords, nil
}
func HandleAlgoliaWorkflowSearchByUser(ctx context.Context, userId string) ([]AlgoliaSearchWorkflow, error) {
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined")
return []AlgoliaSearchWorkflow{}, errors.New("Algolia keys not defined")
}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("workflows")
appSearch := fmt.Sprintf("%s", userId)
res, err := algoliaIndex.Search(appSearch)
if err != nil {
log.Printf("[WARNING] Failed app searching Algolia for creators: %s", err)
return []AlgoliaSearchWorkflow{}, err
}
var newRecords []AlgoliaSearchWorkflow
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia with app creators: %s", err)
return []AlgoliaSearchWorkflow{}, err
}
//log.Printf("[INFO] Algolia hits for %s: %d", appSearch, len(newRecords))
allRecords := []AlgoliaSearchWorkflow{}
for _, newRecord := range newRecords {
allRecords = append(allRecords, newRecord)
}
return allRecords, nil
}
func HandleAlgoliaAppSearchByUser(ctx context.Context, userId string) ([]AlgoliaSearchApp, error) {
cacheKey := fmt.Sprintf("appsearch_user_%s", userId)
cache, err := GetCache(ctx, cacheKey)
if err == nil {
if cacheData, ok := cache.([]byte); ok {
var cachedApp []AlgoliaSearchApp
err = json.Unmarshal(cacheData, &cachedApp)
if err == nil {
return cachedApp, nil
}
log.Printf("[ERROR] Failed unmarshalling cached app search data in Handle algolia app search for user (%s): %s", cacheKey, err)
}
}
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaSecret) == 0 {
algoliaClient = "JNSS5CFDZZ"
algoliaSecret = os.Getenv("ALGOLIA_PUBLICKEY")
}
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined")
return []AlgoliaSearchApp{}, errors.New("Algolia keys not defined")
}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("appsearch")
returnApps := []AlgoliaSearchApp{}
appSearch := fmt.Sprintf("%s", userId)
res, err := algoliaIndex.Search(appSearch)
if err != nil {
log.Printf("[ERROR] Failed app searching Algolia for creators (%s): %s", appSearch, err)
appData, err := json.Marshal(returnApps)
if err == nil {
SetCache(ctx, cacheKey, appData, 30)
} else {
log.Printf("[ERROR] Failed to marshal Algolia result in handle aloglia search (8): %s", err)
}
return returnApps, err
}
var newRecords []AlgoliaSearchApp
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[ERROR] Failed unmarshaling from Algolia with app creators: %s", err)
appData, err := json.Marshal(returnApps)
if err == nil {
SetCache(ctx, cacheKey, appData, 30)
} else {
log.Printf("[ERROR] Failed to marshal Algolia result in handle aloglia search (9): %s", err)
}
return returnApps, err
}
for _, newRecord := range newRecords {
newAppName := strings.TrimSpace(strings.Replace(newRecord.Name, "_", " ", -1))
newRecord.Name = newAppName
returnApps = append(returnApps, newRecord)
}
appData, err := json.Marshal(returnApps)
if err == nil {
SetCache(ctx, cacheKey, appData, 30)
} else {
log.Printf("[ERROR] Failed to marshal Algolia result in handle aloglia search (10): %s", err)
}
return returnApps, nil
}
func HandleAlgoliaCreatorSearch(ctx context.Context, username string) (AlgoliaSearchCreator, error) {
tmpUsername, err := url.QueryUnescape(username)
if err == nil {
username = tmpUsername
}
if strings.HasPrefix(username, "@") {
username = strings.Replace(username, "@", "", 1)
}
username = strings.ToLower(strings.TrimSpace(username))
cacheKey := fmt.Sprintf("algolia_creator_%s", username)
searchCreator := AlgoliaSearchCreator{}
cache, err := GetCache(ctx, cacheKey)
if err == nil {
cacheData := []byte(cache.([]uint8))
//log.Printf("CACHE: %d", len(cacheData))
//log.Printf("CACHEDATA: %#v", cacheData)
err = json.Unmarshal(cacheData, &searchCreator)
if err == nil {
return searchCreator, nil
}
}
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined")
return searchCreator, errors.New("Algolia keys not defined")
}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("creators")
res, err := algoliaIndex.Search(username)
if err != nil {
log.Printf("[ERROR] Failed searching Algolia creators (%s): %s", username, err)
return searchCreator, err
}
var newRecords []AlgoliaSearchCreator
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia creators: %s", err)
return searchCreator, err
}
//log.Printf("RECORDS: %d", len(newRecords))
foundUser := AlgoliaSearchCreator{}
for _, newRecord := range newRecords {
if strings.ToLower(newRecord.Username) == strings.ToLower(username) || newRecord.ObjectID == username || ArrayContainsLower(newRecord.Synonyms, username) {
foundUser = newRecord
break
}
}
// Handling search within a workflow, and in the future, within apps
if len(foundUser.ObjectID) == 0 {
if len(username) == 36 {
// Check workflows
algoliaIndex := algClient.InitIndex("workflows")
res, err := algoliaIndex.Search(username)
if err != nil {
log.Printf("[ERROR] Failed searching Algolia creator workflow (%s): %s", username, err)
return searchCreator, err
}
var newRecords []AlgoliaSearchWorkflow
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia creator workflow: %s", err)
if len(newRecords) > 0 && len(newRecords[0].ObjectID) > 0 {
log.Printf("[INFO] Workflow search ID: %#v", newRecords[0].ObjectID)
} else {
return searchCreator, err
}
}
//log.Printf("[DEBUG] Got %d records for workflow sub", len(newRecords))
if len(newRecords) == 1 {
if len(newRecords[0].Creator) > 0 && username != newRecords[0].Creator {
foundCreator, err := HandleAlgoliaCreatorSearch(ctx, newRecords[0].Creator)
if err != nil {
return searchCreator, err
}
foundUser = foundCreator
} else {
return searchCreator, errors.New("User not found")
}
} else {
return searchCreator, errors.New("User not found")
}
} else {
return searchCreator, errors.New("User not found")
}
}
if project.CacheDb {
data, err := json.Marshal(foundUser)
if err != nil {
return foundUser, nil
}
err = SetCache(ctx, cacheKey, data, 30)
if err != nil {
log.Printf("[WARNING] Failed updating algolia username cache: %s", err)
}
}
return foundUser, nil
}
func HandleAlgoliaPartnerSearch(ctx context.Context, orgId string) (AlgoliaSearchPartner, error) {
cacheKey := fmt.Sprintf("algolia_partner_%s", orgId)
searchPartner := AlgoliaSearchPartner{}
cache, err := GetCache(ctx, cacheKey)
if err == nil {
cacheData := []byte(cache.([]uint8))
err = json.Unmarshal(cacheData, &searchPartner)
if err == nil {
return searchPartner, nil
}
}
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined")
return AlgoliaSearchPartner{}, errors.New("Algolia keys not defined")
}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("partners")
res, err := algoliaIndex.Search(orgId)
if err != nil {
log.Printf("[WARNING] Failed searching Algolia partners: %s", err)
return AlgoliaSearchPartner{}, err
}
var newRecords []AlgoliaSearchPartner
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia partners: %s", err)
return AlgoliaSearchPartner{}, err
}
foundPartner := AlgoliaSearchPartner{}
for _, newRecord := range newRecords {
if newRecord.OrgId == orgId {
foundPartner = newRecord
break
}
}
if project.CacheDb {
data, err := json.Marshal(foundPartner)
if err != nil {
return foundPartner, nil
}
err = SetCache(ctx, cacheKey, data, 30)
if err != nil {
log.Printf("[WARNING] Failed updating algolia partner cache: %s", err)
}
}
return foundPartner, nil
}
func HandleAlgoliaCreatorUpload(ctx context.Context, user User, overwrite bool, isOrg bool) (string, error) {
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined")
return "", errors.New("Algolia keys not defined")
}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("creators")
res, err := algoliaIndex.Search(user.Id)
if err != nil {
log.Printf("[ERROR] Failed searching Algolia creators (%s): %s", user.Id, err)
return "", err
}
var newRecords []AlgoliaSearchCreator
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia creators: %s", err)
return "", err
}
//log.Printf("RECORDS: %d", len(newRecords))
for _, newRecord := range newRecords {
if newRecord.ObjectID == user.Id {
log.Printf("[INFO] Object %s already exists in Algolia", user.Id)
if overwrite {
break
} else {
return user.Id, errors.New("User ID already exists!")
}
}
}
timeNow := int64(time.Now().Unix())
records := []AlgoliaSearchCreator{
AlgoliaSearchCreator{
ObjectID: user.Id,
TimeEdited: timeNow,
Image: user.PublicProfile.GithubAvatar,
Username: user.PublicProfile.GithubUsername,
IsOrg: isOrg,
},
}
_, err = algoliaIndex.SaveObjects(records)
if err != nil {
log.Printf("[WARNING] Algolia Object put err: %s", err)
return "", err
}
log.Printf("[INFO] SUCCESSFULLY UPLOADED creator %s with ID %s TO ALGOLIA!", user.Username, user.Id)
return user.Id, nil
}
func HandleAlgoliaCreatorDeletion(ctx context.Context, userId string) error {
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined")
return errors.New("Algolia keys not defined")
}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("creators")
res, err := algoliaIndex.Search(userId)
if err != nil {
log.Printf("[ERROR] Failed searching Algolia creators (%s): %s", userId, err)
return err
}
var newRecords []AlgoliaSearchCreator
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia creators: %s", err)
return err
}
//log.Printf("RECORDS: %d", len(newRecords))
foundItem := AlgoliaSearchCreator{}
for _, newRecord := range newRecords {
if newRecord.ObjectID == userId {
foundItem = newRecord
break
}
}
// Should delete it?
if len(foundItem.ObjectID) > 0 {
_, err = algoliaIndex.DeleteObject(foundItem.ObjectID)
if err != nil {
log.Printf("[WARNING] Algolia Creator delete problem: %s", err)
return err
}
log.Printf("[INFO] Successfully removed creator %s with ID %s FROM ALGOLIA!", foundItem.Username, userId)
}
return nil
}
// Usecase Algolia Upload
func HandleAlgoliaUsecaseUpload(ctx context.Context, usecase UsecaseInfo, overwrite bool) (string, error) {
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined")
return "", errors.New("Algolia keys not defined")
}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("usecases")
res, err := algoliaIndex.Search(usecase.Id)
if err != nil {
log.Printf("[WARNING] Failed searching Algolia usecases: %s", err)
return "", err
}
var newRecords []AlgoliaSearchUsecase
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia partners: %s", err)
return "", err
}
//log.Printf("RECORDS: %d", len(newRecords))
for _, newRecord := range newRecords {
if newRecord.ObjectID == usecase.Id {
log.Printf("[INFO] Object %s already exists in Algolia", usecase.Id)
if overwrite {
break
} else {
return usecase.Id, errors.New("Usecase ID already exists!")
}
}
}
timeNow := int64(time.Now().Unix())
records := []AlgoliaSearchUsecase{
AlgoliaSearchUsecase{
ObjectID: usecase.Id,
PartnerName: usecase.CompanyInfo.Name,
PartnerId: usecase.CompanyInfo.Id,
Name: usecase.MainContent.Title,
Description: usecase.MainContent.Description,
Categories: usecase.MainContent.Categories,
SourceAppType: usecase.MainContent.SourceAppType,
DestinationAppType: usecase.MainContent.DestinationAppType,
PublicWorkflowID: usecase.MainContent.PublicWorkflowID,
TimeEdited: timeNow,
},
}
_, err = algoliaIndex.SaveObjects(records)
if err != nil {
log.Printf("[WARNING] Algolia Object put err: %s", err)
return "", err
}
log.Printf("[INFO] SUCCESSFULLY UPLOADED partner %s with ID %s TO ALGOLIA!", usecase.MainContent.Title, usecase.Id)
return usecase.Id, nil
}
// Usecase deletion
func HandleAlgoliaUsecaseDeletion(ctx context.Context, usecaseId string) error {
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined")
return errors.New("Algolia keys not defined")
}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("usecases")
res, err := algoliaIndex.Search(usecaseId)
if err != nil {
log.Printf("[ERROR] Failed searching Algolia usecases (%s): %s", usecaseId, err)
return err
}
var newRecords []AlgoliaSearchUsecase
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia usecases: %s", err)
return err
}
//log.Printf("RECORDS: %d", len(newRecords))
foundItem := AlgoliaSearchUsecase{}
for _, newRecord := range newRecords {
if newRecord.ObjectID == usecaseId {
foundItem = newRecord
break
}
}
// Should delete it?
if len(foundItem.ObjectID) > 0 {
_, err = algoliaIndex.DeleteObject(foundItem.ObjectID)
if err != nil {
log.Printf("[WARNING] Algolia Usecase delete problem: %s", err)
return err
}
log.Printf("[INFO] Successfully removed usecase %s with ID %s FROM ALGOLIA!", foundItem.Name, usecaseId)
}
return nil
}
// Shitty temorary system
// Adding schedule to run over with another algorithm
// as well as this one, as to increase priority based on popularity:
// searches, clicks & conversions (CTR)
func GetWorkflowPriority(workflow Workflow) int {
prio := 0
if len(workflow.Tags) > 2 {
prio += 1
}
if len(workflow.Name) > 5 {
prio += 1
}
if len(workflow.Description) > 100 {
prio += 1
}
if len(workflow.WorkflowType) > 0 {
prio += 1
}
if len(workflow.UsecaseIds) > 0 {
prio += 3
}
if len(workflow.Comments) >= 2 {
prio += 2
}
return prio
}
func handleAlgoliaWorkflowUpdate(ctx context.Context, workflow Workflow) (string, error) {
log.Printf("[INFO] Should try to UPLOAD the Workflow to Algolia")
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaClient) == 0 || len(algoliaSecret) == 0 {
log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined")
return "", errors.New("Algolia keys not defined")
}
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("workflows")
//res, err := algoliaIndex.Search("%s", api.ID)
res, err := algoliaIndex.Search(workflow.ID)
if err != nil {
log.Printf("[ERROR] Failed searching Algolia (%s): %s", workflow.ID, err)
return "", err
}
var newRecords []AlgoliaSearchWorkflow
err = res.UnmarshalHits(&newRecords)
if err != nil {
log.Printf("[WARNING] Failed unmarshaling from Algolia workflow upload: %s", err)
return "", err
}
found := false
record := AlgoliaSearchWorkflow{}
for _, newRecord := range newRecords {
if newRecord.ObjectID == workflow.ID {
log.Printf("[INFO] Workflow Object %s already exists in Algolia", workflow.ID)
record = newRecord
found = true
break
}
}
if !found {
return "", errors.New(fmt.Sprintf("Couldn't find public workflow for ID %s", workflow.ID))
}
record.TimeEdited = int64(time.Now().Unix())
categories := []string{}
actions := []string{}
triggers := []string{}
actionRefs := []ActionReference{}
for _, action := range workflow.Actions {
if !ArrayContains(actions, action.AppName) {
// Using this API as the original is kinda stupid
foundApps, err := HandleAlgoliaAppSearchByUser(ctx, action.AppName)
if err == nil && len(foundApps) > 0 {
actionRefs = append(actionRefs, ActionReference{
Name: foundApps[0].Name,
Id: foundApps[0].ObjectID,
ImageUrl: foundApps[0].ImageUrl,
ActionName: []string{action.Name},
})
}
actions = append(actions, action.AppName)
} else {
for refIndex, ref := range actionRefs {
if ref.Name == action.AppName {
if !ArrayContains(ref.ActionName, action.Name) {
actionRefs[refIndex].ActionName = append(actionRefs[refIndex].ActionName, action.Name)
}
}
}
}
}
for _, trigger := range workflow.Triggers {
if !ArrayContains(triggers, trigger.TriggerType) {
triggers = append(triggers, trigger.TriggerType)
}
}
if workflow.WorkflowType != "" {
record.Type = workflow.WorkflowType
}
record.Name = workflow.Name
record.Description = workflow.Description
record.UsecaseIds = workflow.UsecaseIds
record.Triggers = triggers
record.Actions = actions
record.TriggerAmount = len(triggers)
record.ActionAmount = len(actions)
record.Tags = workflow.Tags
record.Categories = categories
record.ActionReferences = actionRefs
record.Priority = GetWorkflowPriority(workflow)
record.Validated = workflow.Validated
if len(workflow.Owner) > 0 {
record.Creator = workflow.Owner
}
records := []AlgoliaSearchWorkflow{
record,
}
//log.Printf("[WARNING] Returning before upload with data %#v", records)
//return records[0].ObjectID, nil
//return "", errors.New("Not prepared yet!")
_, err = algoliaIndex.SaveObjects(records)
if err != nil {
log.Printf("[WARNING] Algolia Object update err: %s", err)
return "", err
}
return workflow.ID, nil
}
// Returns an error if the users' org is over quota
func ValidateExecutionUsage(ctx context.Context, orgId string) (*Org, error) {
if len(orgId) == 0 {
return nil, errors.New("Org ID is empty")
}
org, err := GetOrg(ctx, orgId)
if err != nil {
return org, errors.New(fmt.Sprintf("Failed getting the organization %s: %s", orgId, err))
}
orgStats, err := GetOrgStatistics(ctx, orgId)
if err != nil {
log.Printf("[WARNING] Failed getting org statistics for %s (%s): %s", org.Name, org.Id, err)
return org, nil
}
if org.Billing.AppRunsHardLimit > 0 && orgStats.MonthlyAppExecutions > org.Billing.AppRunsHardLimit {
//log.Printf("[WARNING] Hard limit reached for org %s (%s) during exec start", org.Name, org.Id)
return org, errors.New(fmt.Sprintf("Org %s (%s) has exceeded the app runs hard limit (%d/%d). Your Parent organization can control this.", org.Name, org.Id, orgStats.MonthlyAppExecutions, org.Billing.AppRunsHardLimit))
}
validationOrg := org
validationOrgStats := orgStats
if len(org.CreatorOrg) > 0 {
validationOrg, err = GetOrg(ctx, org.CreatorOrg)
if err != nil {
log.Printf("[WARNING] Failed getting creator org %s (%s): %s ", validationOrg.Name, validationOrg.Id, err)
//return org, errors.New(fmt.Sprintf("Failed getting the creator organization %s: %s", org.CreatorOrg, err))
return org, nil
}
validationOrgStats, err = GetOrgStatistics(ctx, org.CreatorOrg)
if err != nil {
log.Printf("[WARNING] Failed getting creator org statistics for %s (%s): %s ", validationOrg.Name, validationOrg.Id, err)
//return org, errors.New(fmt.Sprintf("Failed getting the creator organization statistics %s: %s", validationOrg.CreatorOrg, err))
return org, nil
}
}
// Fix Me: Add daily stats update script to append daily stats immdediately after day change and reset monthly stats on month change
lastMonthlyReset := validationOrgStats.LastMonthlyResetMonth
currentMonth := time.Now().UTC().Month()
if int(lastMonthlyReset) != int(currentMonth) {
validationOrgStats = handleDailyCacheUpdate(validationOrgStats)
err = SetOrgStatistics(ctx, *validationOrgStats, validationOrg.Id)
if err != nil {
log.Printf("[ERROR] Failed setting org statistics for monthly reset for %s (%s): %s ", validationOrg.Name, validationOrg.Id, err)
}
}
totalAppExecutions := validationOrgStats.MonthlyAppExecutions + validationOrgStats.MonthlyChildAppExecutions
if validationOrg.Billing.InternalAppRunsHardLimit > 0 && totalAppExecutions > validationOrg.Billing.InternalAppRunsHardLimit {
return validationOrg, errors.New(fmt.Sprintf("Org %s (%s) has exceeded app runs hard limit (%d/%d) - Only Shuffle Support can control this metric.", validationOrg.Name, validationOrg.Id, totalAppExecutions, validationOrg.Billing.InternalAppRunsHardLimit))
}
// Allows partners and POV users to run workflows without limits
if validationOrg.LeadInfo.Internal || validationOrg.LeadInfo.ChannelPartner || validationOrg.LeadInfo.IntegrationPartner || validationOrg.LeadInfo.TechPartner || validationOrg.LeadInfo.DistributionPartner || validationOrg.LeadInfo.ServicePartner {
return validationOrg, nil
}
// If enterprise customer or pov then don't block them
if (validationOrg.LeadInfo.Customer || validationOrg.LeadInfo.POV) && validationOrg.SyncFeatures.AppExecutions.Limit >= 300000 {
return validationOrg, nil
}
if totalAppExecutions >= validationOrg.SyncFeatures.AppExecutions.Limit {
return validationOrg, errors.New(fmt.Sprintf("Org %s (%s) has exceeded the monthly app executions limit (%d/%d)", validationOrg.Name, validationOrg.Id, totalAppExecutions, validationOrg.SyncFeatures.AppExecutions.Limit))
}
if debug {
log.Printf("[INFO] Org %s (%s) has %d/%d app executions this month", validationOrg.Name, validationOrg.Id, totalAppExecutions, validationOrg.SyncFeatures.AppExecutions.Limit)
}
return validationOrg, nil
}
func RedirectUserRequest(w http.ResponseWriter, req *http.Request) {
if project.Environment == "cloud" && gceProject == "shuffler" {
log.Printf("[ERROR] Recursive RedirectRequest for %s", req.RequestURI)
w.WriteHeader(400)
w.Write([]byte(`{"success": false, "reason": "Recursive redirect request detected"}`))
return
}
proxyScheme := "https"
proxyHost := fmt.Sprintf("shuffler.io")
httpClient := &http.Client{
Timeout: 120 * time.Second,
}
body, err := ioutil.ReadAll(req.Body)
if err != nil {
log.Printf("[ERROR] Issue in SSR body proxy: %s", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//req.Body = ioutil.NopCloser(bytes.NewReader(body))
url := fmt.Sprintf("%s://%s%s", proxyScheme, proxyHost, req.RequestURI)
if debug {
log.Printf("[DEBUG] Request (%s) request URL: %s. More: %s", req.Method, url, req.URL.String())
}
proxyReq, err := http.NewRequest(req.Method, url, bytes.NewReader(body))
if err != nil {
log.Printf("[ERROR] Failed handling proxy request: %s", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// We may want to filter some headers, otherwise we could just use a shallow copy
proxyReq.Header = make(http.Header)
for h, val := range req.Header {
proxyReq.Header[h] = val
}
newresp, err := httpClient.Do(proxyReq)
if err != nil {
log.Printf("[ERROR] Issue in SSR newresp for %s - should retry: %s", url, err)
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer newresp.Body.Close()
urlbody, err := ioutil.ReadAll(newresp.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
//log.Printf("RESP: %s", urlbody)
for key, value := range newresp.Header {
//log.Printf("%s %s", key, value)
for _, item := range value {
w.Header().Set(key, item)
}
}
w.WriteHeader(newresp.StatusCode)
w.Write(urlbody)
// Need to clear cache in case user gets updated in db
// with a new session and such. This only forces a new search,
// and shouldn't get them logged out
c, err := req.Cookie("session_token")
if err != nil {
c, err = req.Cookie("__session")
}
// FIXME: What is the point of this cookie checking?
if err == nil {
ctx := GetContext(req)
DeleteCache(ctx, fmt.Sprintf("session_%s", c.Value))
}
}
// Checks if a specific user should have "self" access to a creator user
// A creator user can be both a user and an org, so this got a bit tricky
func CheckCreatorSelfPermission(ctx context.Context, requestUser, creatorUser User, algoliaUser *AlgoliaSearchCreator) bool {
if project.Environment != "cloud" {
return false
}
if creatorUser.Id == requestUser.Id {
return true
} else {
for _, user := range algoliaUser.Synonyms {
if user == requestUser.Id {
return true
}
}
if algoliaUser.IsOrg {
log.Printf("[AUDIT] User %s (%s) is an org. Checking if the current user should have access.", algoliaUser.Username, algoliaUser.ObjectID)
// Get the org and check
org, err := GetOrgByCreatorId(ctx, algoliaUser.ObjectID)
if err != nil {
log.Printf("[WARNING] Couldn't find org for creator %s (%s): %s", algoliaUser.Username, algoliaUser.ObjectID, err)
return false
}
log.Printf("[AUDIT] Found org %s (%s) for creator %s (%s)", org.Name, org.Id, algoliaUser.Username, algoliaUser.ObjectID)
for _, user := range org.Users {
if user.Id == requestUser.Id {
if user.Role == "admin" {
return true
}
break
}
}
}
}
return false
}
// Uploads updates for a workflow to a specific file on git
func SetGitWorkflow(ctx context.Context, workflow Workflow, org *Org) error {
if workflow.BackupConfig.UploadRepo != "" || workflow.BackupConfig.UploadBranch != "" || workflow.BackupConfig.UploadUsername != "" || workflow.BackupConfig.UploadToken != "" {
//log.Printf("\n\n\n[DEBUG] Using workflow backup config for org %s (%s)\n\n\n", org.Name, org.Id)
org.Defaults.WorkflowUploadRepo = workflow.BackupConfig.UploadRepo
org.Defaults.WorkflowUploadBranch = workflow.BackupConfig.UploadBranch
org.Defaults.WorkflowUploadUsername = workflow.BackupConfig.UploadUsername
org.Defaults.WorkflowUploadToken = workflow.BackupConfig.UploadToken
// FIXME: Decrypt here
if workflow.BackupConfig.TokensEncrypted {
log.Printf("[DEBUG] Should realtime decrypt token for org %s (%s)", org.Name, org.Id)
org.Defaults.TokensEncrypted = true
} else {
org.Defaults.TokensEncrypted = false
}
}
if org.Defaults.TokensEncrypted == true {
log.Printf("[DEBUG] Decrypting token for org %s (%s)", org.Name, org.Id)
parsedKey := fmt.Sprintf("%s_upload_token", org.Id)
newValue, err := HandleKeyDecryption([]byte(org.Defaults.WorkflowUploadToken), parsedKey)
if err != nil {
log.Printf("[ERROR] Failed decrypting token for org %s (%s): %s", org.Name, org.Id, err)
} else {
org.Defaults.WorkflowUploadToken = string(newValue)
}
parsedKey = fmt.Sprintf("%s_upload_username", org.Id)
newValue, err = HandleKeyDecryption([]byte(org.Defaults.WorkflowUploadUsername), parsedKey)
if err != nil {
log.Printf("[ERROR] Failed decrypting username for org %s (%s): %s", org.Name, org.Id, err)
} else {
org.Defaults.WorkflowUploadUsername = string(newValue)
}
parsedKey = fmt.Sprintf("%s_upload_repo", org.Id)
newValue, err = HandleKeyDecryption([]byte(org.Defaults.WorkflowUploadRepo), parsedKey)
if err != nil {
log.Printf("[ERROR] Failed decrypting repo for org %s (%s): %s", org.Name, org.Id, err)
} else {
org.Defaults.WorkflowUploadRepo = string(newValue)
}
parsedKey = fmt.Sprintf("%s_upload_branch", org.Id)
newValue, err = HandleKeyDecryption([]byte(org.Defaults.WorkflowUploadBranch), parsedKey)
if err != nil {
log.Printf("[ERROR] Failed decrypting branch for org %s (%s): %s", org.Name, org.Id, err)
} else {
org.Defaults.WorkflowUploadBranch = string(newValue)
}
log.Printf("[DEBUG] Decrypted token for org %s (%s): %s", org.Name, org.Id, newValue)
}
if len(org.Defaults.WorkflowUploadBranch) == 0 {
// Default to 'main' for Azure DevOps, 'master' for others
if strings.Contains(org.Defaults.WorkflowUploadRepo, "dev.azure.com") {
org.Defaults.WorkflowUploadBranch = "main"
} else {
org.Defaults.WorkflowUploadBranch = "master"
}
}
if org.Defaults.WorkflowUploadRepo == "" || org.Defaults.WorkflowUploadToken == "" {
//log.Printf("[DEBUG] Missing Repo/Token during Workflow backup upload for org %s (%s)", org.Name, org.Id)
//return errors.New("Missing repo or token")
return nil
}
org.Defaults.WorkflowUploadRepo = strings.TrimSpace(org.Defaults.WorkflowUploadRepo)
// Remove images from workflow before backup
workflow.Image = ""
for actionIndex, _ := range workflow.Actions {
workflow.Actions[actionIndex].LargeImage = ""
workflow.Actions[actionIndex].SmallImage = ""
}
for triggerIndex, _ := range workflow.Triggers {
workflow.Triggers[triggerIndex].LargeImage = ""
workflow.Triggers[triggerIndex].SmallImage = ""
}
// remove github backup info
workflow.BackupConfig = BackupConfig{}
// Use git to upload the workflow.
workflowData, err := json.MarshalIndent(workflow, "", " ")
if err != nil {
log.Printf("[ERROR] Failed marshalling workflow %s (%s) for git upload: %s", workflow.Name, workflow.ID, err)
return err
}
commitMessage := fmt.Sprintf("User '%s' updated workflow '%s' with status '%s' at %s", workflow.UpdatedBy, workflow.Name, workflow.Status, time.Now().Format("2006-01-02 15:04:05"))
repoURL := org.Defaults.WorkflowUploadRepo
repoURL = strings.TrimPrefix(repoURL, "https://")
repoURL = strings.TrimPrefix(repoURL, "http://")
var location string
var isAzureDevOps bool
if strings.Contains(repoURL, "dev.azure.com") {
isAzureDevOps = true
location = fmt.Sprintf("https://%s", repoURL)
log.Printf("[DEBUG] Detected Azure DevOps repository")
} else {
isAzureDevOps = false
// Only append .git if the URL contains github.com
if strings.Contains(repoURL, "github.com") && !strings.HasSuffix(repoURL, ".git") {
repoURL += ".git"
}
urlEncodedPassword := url.QueryEscape(org.Defaults.WorkflowUploadToken)
location = fmt.Sprintf("https://%s:%s@%s", org.Defaults.WorkflowUploadUsername, urlEncodedPassword, repoURL)
}
maskedRepo := location
if !isAzureDevOps {
maskedRepo = strings.ReplaceAll(location, org.Defaults.WorkflowUploadToken, "****")
if urlEncodedPassword := url.QueryEscape(org.Defaults.WorkflowUploadToken); urlEncodedPassword != org.Defaults.WorkflowUploadToken {
maskedRepo = strings.ReplaceAll(maskedRepo, urlEncodedPassword, "****")
}
}
log.Printf("[DEBUG] Uploading workflow %s to repo: %s", workflow.ID, maskedRepo)
fs := memfs.New()
if len(workflow.Status) == 0 {
workflow.Status = "test"
}
//filePath := fmt.Sprintf("/%s/%s.json", workflow.Status, workflow.ID)
filePath := fmt.Sprintf("%s/%s/%s_%s.json", workflow.ExecutingOrg.Id, workflow.Status, strings.ReplaceAll(workflow.Name, " ", "-"), workflow.ID)
cloneOptions := &git.CloneOptions{
URL: location,
}
// For Azure DevOps, add additional auth configuration and capability handling
if isAzureDevOps {
transport.UnsupportedCapabilities = []capability.Capability{
capability.ThinPack,
}
cloneOptions.Auth = &gitHttp.BasicAuth{
Username: org.Defaults.WorkflowUploadUsername, // Use the username for Azure DevOps
Password: org.Defaults.WorkflowUploadToken,
}
}
repo, err := git.Clone(memory.NewStorage(), fs, cloneOptions)
if err != nil {
errMsg := err.Error()
if isAzureDevOps {
log.Printf("[ERROR] Azure DevOps clone failed. Check: 1) PAT has Code(R/W) permissions, 2) Branch '%s' exists, 3) Repo URL is correct", org.Defaults.WorkflowUploadBranch)
}
log.Printf("[ERROR] Error cloning repo '%s': %s", maskedRepo, strings.ReplaceAll(errMsg, org.Defaults.WorkflowUploadToken, "****"))
return err
}
w, err := repo.Worktree()
if err != nil {
log.Printf("[ERROR] Error getting worktree for repo '%s': %s", maskedRepo, err)
return err
}
// Write the byte blob to the in-memory file system
file, err := fs.Create(filePath)
if err != nil {
log.Printf("[ERROR] Creating file in repo '%s': %v", maskedRepo, err)
return err
}
defer file.Close()
if _, err = io.Copy(file, bytes.NewReader(workflowData)); err != nil {
log.Printf("[ERROR] Writing data to file: %v", err)
return err
}
if _, err = w.Add(filePath); err != nil {
log.Printf("[ERROR] Adding file to staging area: %s", err)
return err
}
// Check if there are any changes to commit
status, err := w.Status()
if err != nil {
log.Printf("[ERROR] Getting working tree status: %v", err)
return err
}
hasChanges := false
for _, fileStatus := range status {
if fileStatus.Staging != git.Unmodified {
hasChanges = true
break
}
}
if !hasChanges {
log.Printf("[INFO] No changes detected for workflow %s (%s). File content is identical to existing version.", workflow.Name, workflow.ID)
return nil
}
authorName := org.Defaults.WorkflowUploadUsername
if authorName == "" {
if isAzureDevOps {
authorName = "Workflow Automation"
} else {
authorName = "Shuffle User"
}
}
_, err = w.Commit(commitMessage, &git.CommitOptions{
Author: &object.Signature{
Name: authorName,
Email: "",
When: time.Now(),
},
})
if err != nil {
log.Printf("[ERROR] Committing changes: %v", err)
return err
}
//log.Printf("[DEBUG] Commit Hash: %s", commit)
// Push the changes to a remote repository (replace URL with your repository URL)
// fmt.Sprintf("refs/heads/%s:refs/heads/%s", org.Defaults.WorkflowUploadBranch, org.Defaults.WorkflowUploadBranch)},
ref := fmt.Sprintf("refs/heads/%s:refs/heads/%s", org.Defaults.WorkflowUploadBranch, org.Defaults.WorkflowUploadBranch)
pushOptions := &git.PushOptions{
RemoteName: "origin",
RefSpecs: []config.RefSpec{config.RefSpec(ref)},
}
// Set authentication for push operations for both Azure DevOps and GitHub
pushOptions.Auth = &gitHttp.BasicAuth{
Username: org.Defaults.WorkflowUploadUsername,
Password: org.Defaults.WorkflowUploadToken,
}
err = repo.Push(pushOptions)
if err != nil {
log.Printf("[ERROR] Git push failed for repo '%s': %v", maskedRepo, err)
return err
}
log.Printf("[DEBUG] Workflow successfully uploaded to '%s'!", maskedRepo)
return nil
}
// Creates osfs from folderpath with a basepath as directory base
func CreateFs(basepath, pathname string) (billy.Filesystem, error) {
log.Printf("[INFO] MemFS base: %s, pathname: %s", basepath, pathname)
fs := memfs.New()
err := filepath.Walk(pathname,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.Contains(path, ".git") {
return nil
}
// Fix the inner path here
newpath := strings.ReplaceAll(path, pathname, "")
fullpath := fmt.Sprintf("%s%s", basepath, newpath)
switch mode := info.Mode(); {
case mode.IsDir():
err = fs.MkdirAll(fullpath, 0644)
if err != nil {
log.Printf("Failed making folder: %s", err)
}
case mode.IsRegular():
srcData, err := ioutil.ReadFile(path)
if err != nil {
log.Printf("Src error: %s", err)
return err
}
dst, err := fs.Create(fullpath)
if err != nil {
log.Printf("Dst error: %s", err)
return err
}
_, err = dst.Write(srcData)
if err != nil {
log.Printf("Dst write error: %s", err)
return err
}
}
return nil
})
return fs, err
}
// Also deactivates. It's a toggle for off and on.
func ActivateWorkflowApp(resp http.ResponseWriter, request *http.Request) {
cors := HandleCors(resp, request)
if cors {
return
}
user, err := HandleApiAuthentication(resp, request)
if err != nil {
log.Printf("[WARNING] Api authentication failed in get active apps: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
if user.Role == "org-reader" {
log.Printf("[WARNING] Org-reader doesn't have access to activate workflow app (shared): %s (%s)", user.Username, user.Id)
resp.WriteHeader(403)
resp.Write([]byte(`{"success": false, "reason": "Read only user"}`))
return
}
ctx := GetContext(request)
location := strings.Split(request.URL.String(), "/")
var appId string
activate := true
shouldDistributeToLocation := false
if location[1] == "api" {
if len(location) <= 4 {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
appId = location[4]
if strings.ToLower(location[5]) == "deactivate" {
activate = false
}
if strings.ToLower(location[5]) == "distribute" {
shouldDistributeToLocation = true
}
}
// If onprem, it should autobuild the container(s) from here
// FIXME: The problem with redirect:
// 1. You are in EU
// 2. You activate an app for a suborg, which has to be saved to ActivatedApps in the org in EU so that it loads properly
// 3. The app itself is in EU - NOT in UK, but the org has to be updated in UK -> EU propagation
// 4. In this case, it would overwrite the current change as well
// Additional: Superfluous response(s)
if project.Environment == "cloud" && gceProject != "shuffler" {
go LoadAppConfigFromMain(appId, false)
}
// This is a special case where auth was handled in another region, and activation is done anyway
if project.Environment == "cloud" && gceProject == "shuffler" && request.URL.Query().Get("propagation") == os.Getenv("SHUFFLE_PROPAGATE_TOKEN") && len(os.Getenv("SHUFFLE_PROPAGATE_TOKEN")) > 0 {
log.Printf("[AUDIT] User %s (%s) is activating app %s for org %s (%s) with their org auth token (distributed)", user.Username, user.Id, appId, user.ActiveOrg.Name, user.ActiveOrg.Id)
org, err := GetOrg(ctx, user.ActiveOrg.Id)
if err != nil {
log.Printf("[ERROR] Failed getting org %s (%s) for app activation: %s (prop)", user.ActiveOrg.Name, user.ActiveOrg.Id, err)
resp.WriteHeader(500)
resp.Write([]byte(`{"success": false, "reason": "Failed getting org"}`))
return
}
added := false
if activate {
if !ArrayContains(org.ActiveApps, appId) {
org.ActiveApps = append(org.ActiveApps, appId)
added = true
} else if ArrayContains(org.ActiveApps, appId) {
// If the app is already in the org, we don't need to add it again
log.Printf("[DEBUG] App %s already exists in org %s (%s). Not adding again (prop)", appId, user.ActiveOrg.Name, user.ActiveOrg.Id)
resp.WriteHeader(200)
resp.Write([]byte(`{"success": true, "reason": "App already exists in org"}`))
return
}
} else {
// Remove from the array
newActiveApps := []string{}
for _, activeApp := range org.ActiveApps {
if activeApp == appId {
continue
}
newActiveApps = append(newActiveApps, activeApp)
}
org.ActiveApps = newActiveApps
added = true
}
if added {
err = SetOrg(ctx, *org, org.Id)
if err != nil {
log.Printf("[ERROR] Failed setting org %s (%s) after activating app %s: %s (propagate!)", user.ActiveOrg.Name, user.ActiveOrg.Id, appId, err)
resp.WriteHeader(500)
resp.Write([]byte(`{"success": true, "reason": "Failed setting org after activating app"}`))
return
}
resp.WriteHeader(200)
resp.Write([]byte(`{"success": true, "reason": "App activated"}`))
return
} else {
resp.WriteHeader(201)
resp.Write([]byte(`{"success": true, "reason": "App already handled"}`))
return
}
}
app, err := GetApp(ctx, appId, user, false)
if err != nil {
appName := request.URL.Query().Get("app_name")
appVersion := request.URL.Query().Get("app_version")
if len(appName) > 0 && len(appVersion) > 0 {
apps, err := FindWorkflowAppByName(ctx, appName)
//log.Printf("[INFO] Found %d apps for %s", len(apps), appName)
if err != nil || len(apps) == 0 {
log.Printf("[WARNING] Error getting app from name '%s' (app config): %s", appName, err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
return
}
selectedApp := WorkflowApp{}
for _, app := range apps {
if !app.Sharing && !app.Public {
continue
}
if app.Name == appName {
selectedApp = app
}
if app.Name == appName && app.AppVersion == appVersion {
selectedApp = app
}
}
app = &selectedApp
} else {
log.Printf("[WARNING] Error getting app with ID %s (app config): %s", appId, err)
// Automatic propagation to cloud regions
if project.Environment == "cloud" && gceProject != "shuffler" {
app, err := HandleAlgoliaAppSearch(ctx, appId)
if err == nil {
// this means that the app exists. so, let's
// ask our propagator to proagate it further.
log.Printf("[INFO] Found apps %s - %s in algolia", app.Name, app.ObjectID)
if app.ObjectID != appId {
log.Printf("[WARNING] App %s doesn't exist in algolia", appId)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
return
}
// i can in theory, run this without using goroutines
// and then recursively call the same function. but that
// would make this request way too long.
go func() {
err = propagateApp(appId, false)
if err != nil {
log.Printf("[WARNING] Error propagating app %s - %s: %s", app.Name, app.ObjectID, err)
} else {
log.Printf("[INFO] Propagated app %s - %s. Sending request again!", app.Name, app.ObjectID)
}
}()
resp.WriteHeader(202)
resp.Write([]byte(`{"success": false, "reason": "Taking care of some magic. Please try activation again in a few seconds!"}`))
return
} else {
log.Printf("[WARNING] Error getting app %s (algolia): %s", appName, err)
}
} else if project.Environment == "cloud" && gceProject == "shuffler" {
// Automatic deletion in the main region if the app doesn't exist
if len(app.ID) == 0 {
log.Printf("[INFO] Auto-Removing app %s from Algolia as it doesn't exist with the same ID anymore. Request source: %s (%s) in org %s (%s)", appId, user.Username, user.Id, user.ActiveOrg.Name, user.ActiveOrg.Id)
algoliaClient := os.Getenv("ALGOLIA_CLIENT")
algoliaSecret := os.Getenv("ALGOLIA_SECRET")
if len(algoliaClient) > 0 && len(algoliaSecret) > 0 {
algClient := search.NewClient(algoliaClient, algoliaSecret)
algoliaIndex := algClient.InitIndex("appsearch")
_, err = algoliaIndex.DeleteObject(appId)
if err != nil {
resp.WriteHeader(500)
resp.Write([]byte(`{"success": false, "reason": "Failed removing the app from Algolia"}`))
return
}
}
}
}
resp.WriteHeader(400)
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
return
}
}
if activate == false && app.ReferenceOrg == user.ActiveOrg.Id {
resp.WriteHeader(400)
resp.Write([]byte(`{"success": false, "reason": "Can't remove app from current org as it is the owner org."}`))
return
}
distributingApp := false
if !app.Public && app.ReferenceOrg != user.ActiveOrg.Id && user.ActiveOrg.Role == "admin" {
if app.Owner == user.Id {
log.Printf("[INFO] App %s (%s) is owned by the user %s (%s). Distributing it to the org %s (%s)", app.Name, app.ID, user.Username, user.Id, user.ActiveOrg.Name, user.ActiveOrg.Id)
distributingApp = true
} else {
// check if the app belongs to parent org
org, err := GetOrg(ctx, user.ActiveOrg.Id)
if err != nil {
log.Printf("[ERROR] Failed getting org %s (%s): %s", user.ActiveOrg.Name, user.ActiveOrg.Id, err)
resp.WriteHeader(500)
resp.Write([]byte(`{"success": false, "reason": "Failed getting org"}`))
return
}
if org.CreatorOrg == app.ReferenceOrg {
log.Printf("[INFO] App %s (%s) is owned by the parent org %s (%s). Distributing it to the suborg %s (%s)", app.Name, app.ID, org.Name, org.Id, user.ActiveOrg.Name, user.ActiveOrg.Id)
distributingApp = true
}
}
}
org := &Org{}
added := false
if app.Sharing || app.Public || !activate || distributingApp {
org, err = GetOrg(ctx, user.ActiveOrg.Id)
if err == nil {
if len(org.ActiveApps) > 150 {
// No reason for it to be this big. Arbitrarily reducing.
same := []string{}
samecnt := 0
for _, activeApp := range org.ActiveApps {
if ArrayContains(same, activeApp) {
samecnt += 1
continue
}
same = append(same, activeApp)
}
added = true
//log.Printf("Same: %d, total uniq: %d", samecnt, len(same))
org.ActiveApps = org.ActiveApps[len(org.ActiveApps)-100 : len(org.ActiveApps)-1]
}
if activate {
if !ArrayContains(org.ActiveApps, app.ID) {
org.ActiveApps = append(org.ActiveApps, app.ID)
added = true
} else if ArrayContains(org.ActiveApps, app.ID) && !app.Public {
// If the app is already in the org, we don't need to add it again
log.Printf("[DEBUG] App %s (%s) already exists in org %s (%s). Not adding again.", app.Name, app.ID, user.ActiveOrg.Name, user.ActiveOrg.Id)
resp.WriteHeader(200)
resp.Write([]byte(`{"success": true, "reason": "App already exists in org"}`))
return
}
} else {
// Remove from the array
newActiveApps := []string{}
for _, activeApp := range org.ActiveApps {
if activeApp == app.ID {
continue
}
newActiveApps = append(newActiveApps, activeApp)
}
org.ActiveApps = newActiveApps
added = true
}
if added {
err = SetOrg(ctx, *org, org.Id)
if err != nil {
log.Printf("[WARNING] Failed setting org when autoadding apps on save: %s", err)
} else {
addRemove := "Added"
if !activate {
addRemove = "Removed"
}
log.Printf("[INFO] %s public app %s (%s) to/from org %s (%s). Activated apps: %d", addRemove, app.Name, app.ID, user.ActiveOrg.Name, user.ActiveOrg.Id, len(org.ActiveApps))
DeleteCache(ctx, fmt.Sprintf("apps_%s", user.Id))
DeleteCache(ctx, fmt.Sprintf("apps_%s", user.ActiveOrg.Id))
if project.Environment == "cloud" && gceProject != "shuffler" {
// propagate org.ActiveApps to the main region
go func() {
// wait for a second before propagating again
log.Printf("[INFO] Propagating org %s after sleeping for a second!", user.ActiveOrg.Id)
time.Sleep(1 * time.Second)
err = propagateOrg(*org, true)
if err != nil {
log.Printf("[WARNING] Error propagating org %s: %s", user.ActiveOrg.Id, err)
}
}()
}
}
}
}
} else {
// Check if the user in the current context should have access to it or not
if user.Id == app.Owner || user.ActiveOrg.Id == app.ReferenceOrg || ArrayContains(app.Contributors, user.Id) {
log.Printf("[AUDIT] User %s (%s) is activating app %s (%s) for org %s (%s)", user.Username, user.Id, app.Name, app.ID, user.ActiveOrg.Name, user.ActiveOrg.Id)
} else if project.Environment == "cloud" && user.Verified == true && user.Active == true && user.SupportAccess == true && strings.HasSuffix(user.Username, "@shuffler.io") {
log.Printf("[AUDIT] User %s (%s) is activating app %s (%s) for org %s (%s) as a support user", user.Username, user.Id, app.Name, app.ID, user.ActiveOrg.Name, user.ActiveOrg.Id)
} else {
foundOrg := &Org{}
for _, org := range user.Orgs {
if org != app.ReferenceOrg {
continue
}
foundOrg, err = GetOrg(ctx, org)
if err != nil {
log.Printf("[ERROR] Failed getting org %s: %s", org, err)
}
break
}
allowed := false
if foundOrg.Id == app.ReferenceOrg {
for _, foundUser := range foundOrg.Users {
if foundUser.Id == user.Id && foundUser.Role != "org-reader" {
allowed = true
break
}
}
}
if !allowed {
log.Printf("[WARNING] User is trying to activate %s which is NOT a public app", app.Name)
resp.WriteHeader(400)
resp.Write([]byte(`{"success": false}`))
return
}
}
}
if shouldDistributeToLocation {
// Distribute to runtime Locations
allEnvironments, err := GetEnvironments(ctx, user.ActiveOrg.Id)
if err != nil {
log.Printf("[ERROR] Failed getting environments for org %s: %s", user.ActiveOrg.Id, err)
resp.WriteHeader(500)
resp.Write([]byte(`{"success": false, "reason": "Failed getting environments"}`))
return
}
relevantEnvironments := []Environment{}
for _, env := range allEnvironments {
if strings.ToLower(env.Type) == "cloud" {
continue
}
if env.Archived {
continue
}
relevantEnvironments = append(relevantEnvironments, env)
}
if len(relevantEnvironments) == 0 {
resp.WriteHeader(400)
resp.Write([]byte(`{"success": false, "reason": "No relevant environments"}`))
return
}
appName := fmt.Sprintf("%s_%s", strings.ToLower(strings.ReplaceAll(app.Name, " ", "-")), app.AppVersion)
if project.Environment == "cloud" {
if app.Public == true {
} else {
appName = fmt.Sprintf("%s_%s", strings.ToLower(strings.ReplaceAll(app.Name, " ", "-")), app.ID)
}
}
for _, env := range relevantEnvironments {
//log.Printf("[INFO] Distributing app %s to environment %s", app.Name, env.Name)
request := ExecutionRequest{
Type: "DOCKER_IMAGE_DOWNLOAD",
ExecutionId: uuid.NewV4().String(),
ExecutionArgument: fmt.Sprintf("frikky/shuffle:%s", appName),
Priority: 11,
}
parsedId := fmt.Sprintf("%s_%s", strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(env.Name, " ", "-"), "_", "-")), env.OrgId)
err = SetWorkflowQueue(ctx, request, parsedId)
if err != nil {
log.Printf("[ERROR] Failed setting workflow queue for env: %s", err)
continue
}
}
resp.WriteHeader(200)
resp.Write([]byte(`{"success": true, "reason": "Re-download request sent to all relevant environments"}`))
return
}
if activate {
log.Printf("[DEBUG] App %s (%s) activated for org %s by user %s (%s). Active apps: %d. Already existed: %t", app.Name, app.ID, user.ActiveOrg.Id, user.Username, user.Id, len(org.ActiveApps), !added)
} else {
log.Printf("[DEBUG] App %s (%s) deactivated for org %s by user %s (%s). Active apps: %d. Already existed: %t", app.Name, app.ID, user.ActiveOrg.Id, user.Username, user.Id, len(org.ActiveApps), !added)
}
DeleteCache(ctx, fmt.Sprintf("apps_%s", user.ActiveOrg.Id))
DeleteCache(ctx, fmt.Sprintf("apps_%s", user.Id))
DeleteCache(ctx, "all_apps")
DeleteCache(ctx, fmt.Sprintf("workflowapps-sorted-100"))
DeleteCache(ctx, fmt.Sprintf("workflowapps-sorted-500"))
DeleteCache(ctx, fmt.Sprintf("workflowapps-sorted-1000"))
reason := "App Activated"
if !activate {
reason = "App Deactivated"
}
// Happens down here as we want to make sure the auth is done
if project.Environment == "cloud" && gceProject != "shuffler" {
if len(os.Getenv("SHUFFLE_PROPAGATE_TOKEN")) > 0 {
newQuery := fmt.Sprintf("propagation=%s", os.Getenv("SHUFFLE_PROPAGATE_TOKEN"))
requestUrl := request.URL.String()
if !strings.Contains(requestUrl, "?") {
requestUrl = fmt.Sprintf("%s?%s", requestUrl, newQuery)
} else {
requestUrl = fmt.Sprintf("%s&%s", requestUrl, newQuery)
}
parsedUrl, err := url.Parse(requestUrl)
if err != nil {
log.Printf("[ERROR] Failed parsing request URL for redirect: %s", err)
} else {
request.URL = parsedUrl
}
}
go RedirectUserRequest(resp, request)
return
// Just to ensure org propagation across regions occurs in time
}
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true,"reason": "%s"}`, reason)))
}
// For replicating HTTP request from schedule user
func HandleSuborgScheduleRun(request *http.Request, workflow *Workflow) {
ctx := context.Background()
if len(workflow.SuborgDistribution) == 0 {
log.Printf("[WARNING] No suborgs to run for workflow %s", workflow.ID)
return
}
// Finding first one.
originalTriggerId := ""
for _, trigger := range workflow.Triggers {
if trigger.TriggerType == "SCHEDULE" {
originalTriggerId = trigger.ID
break
}
}
if len(originalTriggerId) == 0 {
return
}
// 1. Get child workflows of workflow
// 2. Map to the right ones
childWorkflows, err := ListChildWorkflows(ctx, workflow.ID)
if err != nil {
log.Printf("[ERROR] Failed getting child workflows for parent workflow %s: %s", workflow.ID, err)
return
}
client := http.Client{}
for _, childWorkflow := range childWorkflows {
if childWorkflow.ID == workflow.ID {
continue
}
if childWorkflow.OrgId == workflow.OrgId {
continue
}
// Check if the OrgId is still in the workflow.Sub
found := false
for _, suborg := range workflow.SuborgDistribution {
if childWorkflow.OrgId == suborg {
found = true
break
}
}
if !found {
continue
}
// Ensuring the trigger still exists in the child
found = false
for _, trigger := range childWorkflow.Triggers {
if trigger.ReplacementForTrigger == originalTriggerId {
found = true
break
}
}
if !found {
continue
}
log.Printf("[DEBUG] Should be running %s schedule suborg workflows", childWorkflow.ID)
go func(client http.Client, request *http.Request, childWorkflow Workflow) {
baseurl := "https://shuffler.io"
if os.Getenv("BASE_URL") != "" {
baseurl = os.Getenv("BASE_URL")
}
if os.Getenv("SHUFFLE_CLOUDRUN_URL") != "" {
baseurl = os.Getenv("SHUFFLE_CLOUDRUN_URL")
}
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("[ERROR] Failed reading body from schedule request: %s", err)
return
}
request.Body = io.NopCloser(bytes.NewBuffer(body))
formattedUrl := fmt.Sprintf("%s/api/v1/workflows/%s/run", baseurl, childWorkflow.ID)
req, err := http.NewRequest(
"POST",
formattedUrl,
bytes.NewBuffer(body),
)
if err != nil {
log.Printf("[WARNING] Failed mapping child workflow schedule: %s", err)
return
}
for key, value := range request.Header {
req.Header.Set(key, value[0])
}
newresp, err := client.Do(req)
if err != nil {
log.Printf("[ERROR] Failed running child workflow schedule: %s", err)
return
}
defer newresp.Body.Close()
if newresp.StatusCode == 200 {
log.Printf("[DEBUG] Started suborg workflow from schedule. Parent: %s. Child: %s", childWorkflow.ParentWorkflowId, childWorkflow.ID)
} else {
respBody, err := ioutil.ReadAll(newresp.Body)
if err != nil {
log.Printf("[ERROR] Failed to read body from failed newresp")
return
}
log.Printf("[ERROR] Failed to start suborg workflow from schedule with status %d. Parent: %s. Child: %s. Raw Body: %s", newresp.StatusCode, childWorkflow.ParentWorkflowId, childWorkflow.ID, string(respBody))
}
}(client, request, childWorkflow)
}
}
// This is JUST for Singul actions with AI agents.
// As AI Agents can have multiple types of runs, this could change every time.
func RunAgentDecisionSingulActionHandler(execution WorkflowExecution, decision AgentDecision) ([]byte, string, string, []string, string, error) {
debugUrl := ""
log.Printf("[INFO][%s] Running agent decision action '%s' with app '%s'. This is ran with Singul.", execution.ExecutionId, decision.Action, decision.Tool)
// Check if running in test mode
if os.Getenv("AGENT_TEST_MODE") == "true" {
log.Printf("[DEBUG][%s] AGENT_TEST_MODE enabled - using mock tool execution", execution.ExecutionId)
// Call mock handler
body, debugUrl, appName, err := RunAgentDecisionMockHandler(execution, decision)
return body, debugUrl, appName, []string{}, "", err
}
baseUrl := "https://shuffler.io"
if os.Getenv("BASE_URL") != "" {
baseUrl = os.Getenv("BASE_URL")
}
if os.Getenv("SHUFFLE_CLOUDRUN_URL") != "" {
baseUrl = os.Getenv("SHUFFLE_CLOUDRUN_URL")
}
requestUrl := fmt.Sprintf("%s/api/v1/apps/categories/run?authorization=%s&execution_id=%s", baseUrl, execution.Authorization, execution.ExecutionId)
// Change timeout to be 300 seconds (just in case)
// Allows for reruns and self-correcting
client := GetExternalClient(requestUrl)
client.Timeout = 300 * time.Second
newFields := []schemaless.Valuereplace{}
for _, field := range decision.Fields {
newFields = append(newFields, schemaless.Valuereplace{
Key: field.Key,
Value: field.Value,
Answer: field.Answer,
})
}
parsedFields := schemaless.TranslateBadFieldFormats(newFields)
// Check if this is a GET request and strip the body field if present
// GET requests should not have a body and can cause 400 errors
methodValue := ""
for _, field := range parsedFields {
if strings.ToLower(field.Key) == "method" {
methodValue = strings.ToUpper(field.Value)
break
}
}
if (strings.ToLower(decision.Action) == "custom_action" || strings.ToLower(decision.Action) == "api") && strings.ToLower(decision.Tool) != "http" {
var urlValue string
newFields := make([]schemaless.Valuereplace, 0, len(parsedFields))
// Extract url field and keep all non-url fields
for _, field := range parsedFields {
if strings.ToLower(field.Key) == "url" && field.Value != "" {
urlValue = strings.TrimSpace(field.Value)
continue
}
newFields = append(newFields, field)
}
if urlValue != "" {
var path string
if strings.HasPrefix(urlValue, "http://") || strings.HasPrefix(urlValue, "https://") {
if u, err := url.Parse(urlValue); err == nil {
path = u.Path
if u.RawQuery != "" {
path = path + "?" + u.RawQuery
}
}
} else {
path = "/" + strings.TrimLeft(urlValue, "/")
}
if path != "" {
newFields = append(newFields, schemaless.Valuereplace{
Key: "path",
Value: path,
})
if debug {
log.Printf("[DEBUG][%s] Converted url to path for %s: path='%s' (auth base URL will be used)", execution.ExecutionId, decision.Tool, path)
}
}
}
parsedFields = newFields
}
oldFields := []Valuereplace{}
for _, field := range parsedFields {
// Skip body field for GET requests
if methodValue == "GET" && strings.ToLower(field.Key) == "body" {
log.Printf("[INFO][%s] Stripping 'body' field from GET request to %s", execution.ExecutionId, decision.Tool)
continue
}
oldFields = append(oldFields, Valuereplace{
Key: field.Key,
Value: field.Value,
Answer: field.Answer,
})
}
parsedAction := CategoryAction{
AppName: decision.Tool,
Label: decision.Action,
Query: decision.Reason, // Add the reason field for LLM context
Fields: oldFields,
}
if strings.ToLower(decision.Action) == "api" {
parsedAction.Action = "custom_action"
}
marshalledAction, err := json.Marshal(parsedAction)
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed marshalling action in agent decision: %s", execution.ExecutionId, err)
return []byte{}, debugUrl, decision.Tool, []string{}, "", err
}
req, err := http.NewRequest(
"POST",
requestUrl,
bytes.NewBuffer(marshalledAction),
)
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed creating request for agent decision: %s", execution.ExecutionId, err)
return []byte{}, debugUrl, decision.Tool, []string{}, "", err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed running agent decision (1). Timeout: %d: %s", execution.ExecutionId, client.Timeout, err)
return []byte{}, debugUrl, decision.Tool, []string{}, "", err
}
appname := decision.Tool
appId := ""
_ = appId
for key, value := range resp.Header {
//if debug {
// log.Printf("\n\n\n\n[DEBUG][%s] HEADER: key: %s, value: %s\n\n\n\n", execution.ExecutionId, key, value)
//}
if key == "X-Appname" && len(value) > 0 {
appname = value[0]
continue
}
if key == "X-Appid" && len(value) > 0 {
appId = value[0]
continue
}
if key != "X-Debug-Url" {
continue
}
/*
if !strings.HasPrefix(key, "X-") {
continue
}
// Don't care about raw response
if key == "X-Raw-Response-Url" || key == "X-Apprun-Url" {
continue
}
*/
foundValue := ""
for _, val := range value {
if len(val) > 0 {
foundValue = val
break
}
}
debugUrl = foundValue
/*
returnHeaders = append(returnHeaders, Valuereplace{
Key: key,
Value: foundValue,
})
*/
}
originalBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed reading body from agent decision: %s", execution.ExecutionId, err)
return []byte{}, debugUrl, appname, []string{}, "", err
}
body := originalBody
defer resp.Body.Close()
// Try to map it into SchemalessOutput and grab "RawResponse"
outputMapped := SchemalessOutput{}
err = json.Unmarshal(body, &outputMapped)
if err != nil {
log.Printf("[ERROR] AI Agent: Failed unmarshalling agent decision response: %s", err)
return body, debugUrl, appname, []string{}, "", nil
}
if val, ok := outputMapped.RawResponse.(string); ok {
parsedVal, err := base64.StdEncoding.DecodeString(val)
if err == nil && (strings.HasPrefix(string(parsedVal), "{") && strings.HasSuffix(string(parsedVal), "}")) || (strings.HasPrefix(string(parsedVal), "[") && strings.HasSuffix(string(parsedVal), "]")) {
body = parsedVal
} else {
body = []byte(val)
}
} else if val, ok := outputMapped.RawResponse.([]byte); ok {
body = val
} else if val, ok := outputMapped.RawResponse.(map[string]interface{}); ok {
marshalledRawResp, err := json.MarshalIndent(val, "", " ")
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed marshalling agent decision response: %s", execution.ExecutionId, err)
} else {
body = marshalledRawResp
}
} else if outputMapped.RawResponse == nil {
// Do nothing
} else {
log.Printf("[ERROR][%s] AI Agent: FAILED MAPPING RAW RESP INTERfACE. TYPE: %T\n\n\n", execution.ExecutionId, outputMapped.RawResponse)
}
if resp.StatusCode != 200 {
if debug {
log.Printf("[ERROR][%s] AI Agent: Failed running agent decision with status %d: %s", execution.ExecutionId, resp.StatusCode, string(body))
} else {
log.Printf("[ERROR][%s] AI Agent: Failed running agent decision with status %d. Body: %d", execution.ExecutionId, resp.StatusCode, len(body))
}
return body, debugUrl, appname, []string{}, "", errors.New(fmt.Sprintf("Failed running agent decision (2). Status code %d", resp.StatusCode))
}
if outputMapped.Success == false {
return originalBody, debugUrl, appname, []string{}, "", errors.New("Failed running agent decision (3). Success false for Singul action")
}
/*
agentOutput.Decisions[decisionIndex].RunDetails.RawResponse = string(rawResponse)
agentOutput.Decisions[decisionIndex].RunDetails.DebugUrl = debugUrl
if err != nil {
log.Printf("[ERROR] Failed to run agent decision %#v: %s", decision, err)
agentOutput.Decisions[decisionIndex].RunDetails.Status = "FAILED"
resultMapping.Status = "FAILURE"
resultMapping.CompletedAt = time.Now().Unix()
agentOutput.CompletedAt = time.Now().Unix()
} else {
agentOutput.Decisions[decisionIndex].RunDetails.Status = "RUNNING"
}
*/
return body, debugUrl, appname, outputMapped.CategoryLabels, outputMapped.ActionName, nil
}
// Runs an Agent Decision -> returns the result from it
// FIXME: Handle types: https://www.figma.com/board/V6Kg7KxbmuhIUyTImb20t1/Shuffle-AI-Agent-system?node-id=0-1&p=f&t=yIGaSXQYsYReR8cI-0
// This function should handle:
// 1. Running the decided action (user input, Singul, Workflow, Other Agent, Custom HTTP function)
// 2. Taking the result and sending (?) it back
// 3. Ensuring cache for an action is kept up to date
func RunAgentDecisionAction(execution WorkflowExecution, agentOutput AgentOutput, decision AgentDecision) {
defer func() {
if r := recover(); r != nil {
log.Printf("[ERROR] AI_AGENT_PANIC: execution_id=%s decision_id=%s panic=%v", execution.ExecutionId, decision.RunDetails.Id, r)
// Mark decision as failed so agent doesn't get stuck
decision.RunDetails.Status = "FAILURE"
decision.RunDetails.CompletedAt = time.Now().UnixMilli()
decision.RunDetails.RawResponse = fmt.Sprintf("PANIC: %v", r)
}
}()
// Check if it's already ran or not
ctx := context.Background()
decisionId := fmt.Sprintf("agent-%s-%s", execution.ExecutionId, decision.RunDetails.Id)
cache, err := GetCache(ctx, decisionId)
if err == nil {
foundDecision := AgentDecision{}
cacheData := []byte(cache.([]uint8))
err = json.Unmarshal(cacheData, &foundDecision)
if err != nil {
log.Printf("[WARNING][%s] Failed agent decision unmarshal (not critical): %s", execution.ExecutionId, err)
}
if foundDecision.RunDetails.StartedAt > 0 {
log.Printf("[DEBUG][%s] Decision %s already has status '%s'. Returning as it's already started..", execution.ExecutionId, decision.RunDetails.Id, foundDecision.RunDetails.Status)
return
}
}
// Set it to this at the start
if decision.RunDetails.StartedAt <= 0 {
decision.RunDetails.StartedAt = time.Now().UnixMilli()
}
decision.RunDetails.Status = "RUNNING"
marshalledDecision, err := json.Marshal(decision)
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed marshalling decision %s", execution.ExecutionId, decision.RunDetails.Id)
}
go SetCache(ctx, decisionId, marshalledDecision, 300)
if decision.Action == "user_input" || decision.Action == "answer" || decision.Action == "ask" || decision.Action == "question" || decision.Action == "finish" || decision.Category == "standalone" {
} else {
// Singul handler
rawResponse, debugUrl, appname, categoryLabels, actionName, err := RunAgentDecisionSingulActionHandler(execution, decision)
if len(appname) > 0 {
decision.Tool = appname
}
decision.RunDetails.RawResponse = string(rawResponse)
decision.RunDetails.DebugUrl = debugUrl
decision.RunDetails.CategoryLabels = categoryLabels
decision.RunDetails.ActionName = actionName
log.Printf("RawResp: %s", string(rawResponse))
if err != nil {
if debug {
log.Printf("[ERROR][%s] AI Agent: Failed to run agent decision %#v: %s", execution.ExecutionId, decision, err)
} else {
log.Printf("[ERROR][%s] AI Agent: Failed to run agent decision %#v: %s", execution.ExecutionId, decision.RunDetails.Id, err)
}
decision.RunDetails.Status = "FAILURE"
if len(decision.RunDetails.RawResponse) == 0 {
decision.RunDetails.RawResponse = fmt.Sprintf("Failed to start decision action. Raw Error: %s", err)
}
} else {
decision.RunDetails.Status = "FINISHED"
}
// Log individual tool execution result
duration := int64(0)
if decision.RunDetails.CompletedAt > 0 && decision.RunDetails.StartedAt > 0 {
duration = decision.RunDetails.CompletedAt - decision.RunDetails.StartedAt
}
log.Printf("[INFO][%s] AI_AGENT_TOOL: org=%s tool=%s action=%s status=%s duration=%ds", execution.ExecutionId, execution.Workflow.OrgId, decision.Tool, decision.Action, decision.RunDetails.Status, duration)
}
// 1. Send this back as a result for an action
// Then the action itself should decide if it's done or not.
// Would it work to send JUST this decision result?
// This could start the next step(s) automatically?
decision.RunDetails.CompletedAt = time.Now().UnixMilli()
marshalledDecision, err = json.Marshal(decision)
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed marshalling completed decision %s", execution.ExecutionId, decision.RunDetails.Id)
}
go SetCache(ctx, decisionId, marshalledDecision, 300)
// 1. Send an /api/v1/streams request? Due to concurrency, I think this is the only way (?)
// 2. On the streams API, make sure to:
// 1. Check if the execution(s) are finished
// 2. Send the result through AI again to check if it changes (?). Should there be a verdict here?
// 3: Start the next steps of decisions after updates
baseUrl := "https://shuffler.io"
if os.Getenv("BASE_URL") != "" {
baseUrl = os.Getenv("BASE_URL")
}
if os.Getenv("SHUFFLE_CLOUDRUN_URL") != "" {
baseUrl = os.Getenv("SHUFFLE_CLOUDRUN_URL")
}
//url := fmt.Sprintf("%s/api/v1/apps/categories/run?authorization=%s&execution_id=%s", baseUrl, execution.Authorization, execution.ExecutionId)
url := fmt.Sprintf("%s/api/v1/streams", baseUrl)
log.Printf("[DEBUG][%s] Sending agent decision response %s with status %s. Node: %s. URL: %s", execution.ExecutionId, decision.RunDetails.Id, decision.RunDetails.Status, agentOutput.NodeId, url)
//?authorization=%s&execution_id=%s", baseUrl, execution.Authorization, execution.ExecutionId)
client := GetExternalClient(url)
// This is exactly how results for decisions as sent huh
// May need to do this for agentic question, answers as well
parsedAction := ActionResult{
ExecutionId: execution.ExecutionId,
Authorization: execution.Authorization,
// Map in the node ID (action ID) and decision ID to set/continue the right result
Action: Action{
AppName: "AI Agent",
Label: fmt.Sprintf("Agent Decision %s", decision.RunDetails.Id),
ID: agentOutput.NodeId,
},
Status: fmt.Sprintf("agent_%s", decision.RunDetails.Id),
Result: string(marshalledDecision),
}
for _, action := range execution.Workflow.Actions {
if action.ID == parsedAction.Action.ID {
parsedAction.Action = action
break
}
}
marshalledAction, err := json.Marshal(parsedAction)
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed marshalling action in agent decision: %s", execution.ExecutionId, err)
return
}
req, err := http.NewRequest(
"POST",
url,
bytes.NewBuffer(marshalledAction),
)
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed agent decision request creation: %s", execution.ExecutionId, err)
return
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed sending agent decision result: %s", execution.ExecutionId, err)
return
}
foundBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("[ERROR][%s] AI Agent: Failed reading body from agent decision: %s", execution.ExecutionId, err)
return
}
if resp.StatusCode != 200 {
log.Printf("[ERROR][%s] AI Agent: Status %d for decision %s. Body: %s", execution.ExecutionId, resp.StatusCode, decision.RunDetails.Id, string(foundBody))
}
}
func HandleCloudSyncAuthentication(resp http.ResponseWriter, request *http.Request) (SyncKey, error) {
apikey := request.Header.Get("Authorization")
if len(apikey) > 0 {
apikey = strings.Replace(apikey, " ", " ", -1)
if !strings.HasPrefix(apikey, "Bearer ") {
log.Printf("[WARNING] Apikey doesn't start with bearer: %s", apikey)
return SyncKey{}, errors.New("No bearer token for authorization header")
}
apikeyCheck := strings.Split(apikey, " ")
if len(apikeyCheck) != 2 {
log.Printf("[WARNING] Invalid format for apikey: %s", apikeyCheck)
return SyncKey{}, errors.New("Invalid format for apikey")
}
newApikey := apikeyCheck[1]
ctx := GetContext(request)
org, err := getSyncApikey(ctx, newApikey)
if err != nil {
log.Printf("[WARNING] Error in sync check: %s", err)
return SyncKey{}, errors.New(fmt.Sprintf("Error finding key: %s", err))
}
return SyncKey{Apikey: newApikey, OrgId: org}, nil
}
return SyncKey{}, errors.New("Missing authentication")
}
func HandleOrborusFailover(ctx context.Context, request *http.Request, resp http.ResponseWriter, env *Environment) error {
if len(env.Id) == 0 || len(env.Name) == 0 {
// Avoiding this onprem as it doesn't make sense
if project.Environment != "cloud" {
return nil
}
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Environment ID or Name is not set"}`)))
return errors.New("Environment ID or Name is not set")
}
orborusLabel := request.Header.Get("x-orborus-label")
var orboruserr error
var orborusData OrborusStats
body, bodyerr := ioutil.ReadAll(request.Body)
if bodyerr == nil {
orboruserr := json.Unmarshal(body, &orborusData)
if !env.SensorGroup && orboruserr == nil {
if time.Now().Unix() > env.Checkin+90 {
if debug {
log.Printf("[DEBUG] Failover orborus to '%s'. Checkin: %d. Edit: %d", orborusData.Uuid, env.Checkin, env.Edited)
}
env.OrborusUuid = orborusData.Uuid
}
if env.OrborusUuid != orborusData.Uuid && len(env.OrborusUuid) > 0 {
resp.WriteHeader(409)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Orborus UUID mismatch. This means another Orborus (Leader) is already handling this Runtime Location queue. This persists past a few minutes with only one Orborus running, please contact support@shuffler.io"}`)))
return errors.New("Orborus UUID mismatch")
} else {
//env.Checkin = time.Now().Unix()
}
}
}
// Handles a group of hosts running Orborus based on this page:
// https://security.shuffler.io/assets
if env.SensorGroup {
if len(orborusData.Uuid) == 0 || len(orborusData.SensorDetails.Hostname) == 0 {
if debug {
log.Printf("[DEBUG] Orborus data missing UUID or Hostname for sensor group environment '%s' (%s). Orborus Data: %#v", env.Name, env.Id, orborusData)
}
return nil
}
if strings.Contains(orborusData.SensorDetails.Hostname, ".") {
parsedHostnameSplit := strings.Split(orborusData.SensorDetails.Hostname, ".")
orborusData.SensorDetails.Hostname = parsedHostnameSplit[0]
}
// 1 month timeout before removed from the list. We only store
// minimal data anyway, so it really shouldn't matter
hostTimeout := int64(2592000)
hostRefresh := int64(90)
timeNow := int64(time.Now().Unix())
// Using cache to not have to constantly update the environment for every host
// This should fix itself over time (eventual completeness)
checkinKey := fmt.Sprintf("sensor_%s_%s_%s_checkin", env.Name, orborusData.SensorDetails.Hostname, orborusData.SensorDetails.Arch)
timeNowString := fmt.Sprintf("%d", timeNow)
SetCache(ctx, checkinKey, []byte(timeNowString), 120)
removeIndex := []int{}
found := false
updateMade := false
// Just some deduping in case
foundHosts := []string{}
for hostIndex, host := range env.SensorHosts {
parsedHost := fmt.Sprintf("%s-%s", host.Hostname, host.Arch)
if ArrayContains(foundHosts, parsedHost) {
removeIndex = append(removeIndex, hostIndex)
continue
}
foundHosts = append(foundHosts, parsedHost)
}
// Run removeIndex backwards
for i := len(removeIndex) - 1; i >= 0; i-- {
env.SensorHosts = append(env.SensorHosts[:removeIndex[i]], env.SensorHosts[removeIndex[i]+1:]...)
updateMade = true
}
removeIndex = []int{}
for hostIndex, host := range env.SensorHosts {
// Check if more than 90 seconds ago
if host.Hostname == orborusData.SensorDetails.Hostname && host.Arch == orborusData.SensorDetails.Arch {
found = true
if timeNow > host.Checkin+hostRefresh || env.SensorHosts[hostIndex].Uuid != orborusData.Uuid {
if debug {
//log.Printf("[DEBUG] Sensor '%s' in group environment '%s' (%s) is refreshing its checkin. Previous checkin: %d seconds ago", host.Hostname, env.Name, env.Id, timeNow-host.Checkin)
}
updateMade = true
env.SensorHosts[hostIndex].Checkin = timeNow
env.SensorHosts[hostIndex].Uuid = orborusData.Uuid
// FIXME: This needs to be a bit smarter
// For now we will just keep whatever we get first. Any restart
// of the agent will change it.
if host.Uuid != orborusData.Uuid {
DeleteCache(ctx, fmt.Sprintf("sensorupdate_%s_%s", orborusData.SensorDetails.Hostname, orborusData.SensorDetails.Arch))
env.SensorHosts[hostIndex].AutomaticScreenlockEnabled = orborusData.SensorDetails.AutomaticScreenlockEnabled
env.SensorHosts[hostIndex].HdEncrypted = orborusData.SensorDetails.HdEncrypted
env.SensorHosts[hostIndex].LogForwarding = orborusData.SensorDetails.LogForwarding
env.SensorHosts[hostIndex].ResponseActions = orborusData.SensorDetails.ResponseActions
if len(orborusData.SensorDetails.Serial) > 0 {
env.SensorHosts[hostIndex].Serial = orborusData.SensorDetails.Serial
}
if len(orborusData.SensorDetails.InstalledSoftware) > 0 {
env.SensorHosts[hostIndex].InstalledSoftware = orborusData.SensorDetails.InstalledSoftware
}
if len(orborusData.SensorDetails.InstalledSoftware) > 0 {
env.SensorHosts[hostIndex].CodeScanner = orborusData.SensorDetails.CodeScanner
}
}
}
break
}
}
// Appending a new one
if !found {
if debug {
log.Printf("\n\n[DEBUG] Adding new sensor host '%s' to group environment '%s' (%s). Total hosts: %d\n\n", orborusData.SensorDetails.Hostname, env.Name, env.Id, len(env.SensorHosts)+1)
}
updateMade = true
newHost := orborusData.SensorDetails
newHost.Uuid = orborusData.Uuid
newHost.Checkin = timeNow
env.SensorHosts = append(env.SensorHosts, newHost)
}
// Updates at that point (2 minutes~) as long as a single sensor is sending data.
if !updateMade && env.Checkin > 0 && timeNow > env.Checkin+120 {
updateMade = true
}
if updateMade && len(env.SensorHosts) >= 1 {
if debug {
//log.Printf("[DEBUG] Updating sensor host data for group environment '%s' (%s). Total hosts: %d. Checkin: %d seconds ago\n\n", env.Name, env.Id, len(env.SensorHosts), timeNow-env.Checkin)
}
// Sideloading from shuffle-security_sensors instead
removeIndex = []int{}
for sensorIndex, sensor := range env.SensorHosts {
if sensor.Hostname == orborusData.SensorDetails.Hostname && sensor.Arch == orborusData.SensorDetails.Arch {
sensor.Checkin = timeNow
orborusData.SensorDetails.Checkin = timeNow
} else {
checkinKeyCheck := fmt.Sprintf("sensor_%s_%s_%s_checkin", env.Name, sensor.Hostname, sensor.Arch)
foundCache, err := GetCache(ctx, checkinKeyCheck)
if err == nil {
cacheData := string(foundCache.([]uint8))
timestamp, err := strconv.Atoi(cacheData)
if err == nil && timestamp > 0 {
sensor.Checkin = int64(timestamp)
} else {
log.Printf("\n\n[ERROR] Failed ATOI for timestamp of %s: %s. Output: %s\n\n", sensor.Hostname, err, cacheData)
}
}
}
// Check if more than X minutes ago to time out hosts
if timeNow > sensor.Checkin+hostTimeout {
removeIndex = append(removeIndex, sensorIndex)
continue
}
// To reset the data that the env has
// as it doesn't need that much
env.SensorHosts[sensorIndex] = SensorDetails{
Hostname: sensor.Hostname,
Arch: sensor.Arch,
Uuid: sensor.Uuid,
Checkin: sensor.Checkin,
}
}
// Last cleanup
for i := len(removeIndex) - 1; i >= 0; i-- {
env.SensorHosts = append(env.SensorHosts[:removeIndex[i]], env.SensorHosts[removeIndex[i]+1:]...)
log.Printf("[INFO] Sensor '%s' removed from group environment '%s' (%s) due to inactivity. Checkin: %d seconds ago", env.SensorHosts[removeIndex[i]].Hostname, env.Name, env.Id, timeNow-env.SensorHosts[removeIndex[i]].Checkin)
}
go HandleSensorDatastoreUpdate(orborusData)
env.Checkin = timeNow
err := SetEnvironment(ctx, env)
if err != nil {
log.Printf("[ERROR] Sensor group environment '%s' (%s) FAILED to update with new sensor host data. Checkin: %d. Total hosts: %d. Error: %s", env.Name, env.Id, env.Checkin, len(env.SensorHosts), err)
}
}
return nil
}
timeNow := time.Now().Unix()
if request.Method == "POST" {
// Updates every 60 seconds~
if time.Now().Unix() > env.Checkin+60 {
// Print 1/10 times
if rand.Intn(10) == 0 {
log.Printf("[INFO] Updating environment '%s' (%s) from Orborus checkin (60 sec timeout). Previous checkin: %d seconds ago", env.Name, env.Id, timeNow-env.Checkin)
}
env.RunningIp = GetRequestIp(request)
// Orborus label = custom label for Orborus
if len(orborusLabel) > 0 {
env.RunningIp = orborusLabel
}
// Set the checkin cache
if bodyerr == nil && orboruserr == nil {
orborusData.RunningIp = env.RunningIp
env.OrborusUuid = orborusData.Uuid
marshalled, err := json.Marshal(orborusData)
if err == nil {
// Store for a full day. It's reset anyway in the UI at a certain point
cacheKey := fmt.Sprintf("queueconfig-%s-%s", env.Name, env.OrgId)
go SetCache(context.Background(), cacheKey, marshalled, 1440)
}
if orborusData.Swarm {
//env.Licensed = true
env.RunType = "docker"
}
if orborusData.Kubernetes {
env.RunType = "k8s"
}
orborusData.DataLake = env.DataLake
}
env.Checkin = timeNow
err := SetEnvironment(ctx, env)
if err != nil {
log.Printf("[ERROR] Failed updating environment: %s", err)
}
} else {
//if debug {
// log.Printf("[DEBUG] NOT updating env %s yet: %d seconds since checkin", env.Name, timeNow-env.Checkin)
//}
}
}
if env.Archived {
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't use archived environments. Make a new environment or restore the existing one."}`)))
return errors.New("Environment is archived")
}
return nil
}
// Sets sensor details in the org that they belong to
func HandleSensorDatastoreUpdate(orborusDetails OrborusStats) {
if len(orborusDetails.SensorDetails.Hostname) == 0 || orborusDetails.OrgId == "" {
if debug {
log.Printf("[DEBUG] Not updating datastore for sensor without hostname/orgId.")
}
return
}
sensorDetails := orborusDetails.SensorDetails
ctx := context.Background()
// MAX every 60 minutes, or if a sensor is restarted
cacheKey := fmt.Sprintf("sensorupdate_%s_%s", sensorDetails.Hostname, sensorDetails.Arch)
GotCache, err := GetCache(ctx, cacheKey)
if err == nil && GotCache != nil {
//if debug {
// log.Printf("[DEBUG] Skipping datastore update for sensor '%s' as it was updated recently (cache hit)", sensorDetails.Hostname)
//}
return
}
// 24 hour updates. Don't want to overload it.
SetCache(ctx, cacheKey, []byte("1"), 1440)
datastoreSensorIndex := "shuffle-security_sensors"
datastorePackageIndex := "shuffle-security_packages"
// Sets the current sensor details raw
sensorDetails.Checkin = time.Now().Unix()
parsedHostname := strings.TrimSpace(strings.ReplaceAll(strings.ToUpper(sensorDetails.Hostname), " ", "_"))
skippedAmount := 0
maxSoftwareAmount := 1000
handledKeys := []string{}
softwareWg := sync.WaitGroup{}
datastoreSoftwareIndex := "shuffle-security_software"
softwareAmount := len(sensorDetails.InstalledSoftware)
if softwareAmount > maxSoftwareAmount {
softwareAmount = maxSoftwareAmount
}
softwareKeys := make(chan CacheKeyData, softwareAmount)
for softwareCnt, software := range sensorDetails.InstalledSoftware {
if softwareCnt+skippedAmount > maxSoftwareAmount {
break
}
// linux/macos/windows handler
if softwareCnt == 0 && strings.Contains(software.Name, " ") {
software.Name = strings.ReplaceAll(software.Name, software.Version, "")
nameSplit := strings.Split(software.Name, " ")
software.Name = nameSplit[0]
for _, part := range nameSplit[1:] {
if len(part) <= 1 {
continue
}
software.Version = fmt.Sprintf("%s-%s", software.Version, part)
}
}
parsedKeyname := fmt.Sprintf("%s_%s", strings.TrimSpace(strings.ReplaceAll(strings.ToLower(software.Name), " ", "_")), sensorDetails.OS)
if ArrayContains(handledKeys, software.Name) {
skippedAmount += 1
continue
}
handledKeys = append(handledKeys, software.Name)
softwareWg.Add(1)
go func(parsedKeyname string, software Software) {
defer softwareWg.Done()
// 1. Get existing key
// 2. Update Versions & Hostnames
// 3. If it existed already, don't update the "Last Seen" field (or set it to the oldest of the two)
software.OS = sensorDetails.OS
software.Hostnames = []HostDetails{
HostDetails{
Hostname: sensorDetails.Hostname,
Version: software.Version,
UpdatedAt: time.Now().Unix(),
},
}
if len(software.Version) > 0 {
software.Versions = []string{software.Version}
}
datastoreId := fmt.Sprintf("%s_%s_%s", orborusDetails.OrgId, parsedKeyname, datastoreSoftwareIndex)
config, getCacheError := GetDatastoreKey(ctx, datastoreId, datastoreSoftwareIndex)
if getCacheError != nil {
//log.Printf("[ERROR] Failed to get existing datastore key for software '%s': %s", parsedKeyname, getCacheError)
} else if len(config.Value) > 0 {
unmarshalledSoftware := Software{}
err := json.Unmarshal([]byte(config.Value), &unmarshalledSoftware)
if err == nil {
hostExists := false
versionExists := false
for _, foundHost := range unmarshalledSoftware.Hostnames {
if foundHost.Hostname == sensorDetails.Hostname && foundHost.Version == software.Version {
hostExists = true
break
}
}
if !hostExists {
unmarshalledSoftware.Hostnames = append(unmarshalledSoftware.Hostnames, HostDetails{
Hostname: sensorDetails.Hostname,
Version: software.Version,
UpdatedAt: time.Now().Unix(),
})
}
if ArrayContains(unmarshalledSoftware.Versions, software.Version) {
versionExists = true
} else {
unmarshalledSoftware.Versions = append(unmarshalledSoftware.Versions, software.Version)
}
if hostExists && versionExists {
if debug {
//log.Printf("[DEBUG] Software '%s' on host '%s' with version '%s' already exists in datastore. Skipping update.", software.Name, sensorDetails.Hostname, software.Version)
}
softwareKeys <- CacheKeyData{
Key: "",
}
return
}
software = unmarshalledSoftware
}
}
software.Version = ""
parsedValue, err := json.Marshal(software)
if err != nil {
log.Printf("[ERROR] Failed to marshal software for datastore update: %s. Software: %#v", err, software)
softwareKeys <- CacheKeyData{
Key: "",
}
return
}
newKey := CacheKeyData{
Key: parsedKeyname,
Category: datastoreSoftwareIndex,
Value: string(parsedValue),
OrgId: orborusDetails.OrgId,
}
softwareKeys <- newKey
}(parsedKeyname, software)
}
packageAmount := 0
handledKeys = []string{}
for _, curPackage := range sensorDetails.CodeScanner {
if packageAmount > maxSoftwareAmount {
break
}
// Dedups
for _, software := range curPackage.Packages {
if packageAmount > maxSoftwareAmount {
break
}
parsedKeyname := strings.TrimSpace(strings.ReplaceAll(strings.ToLower(software.Name), " ", "_"))
if ArrayContains(handledKeys, parsedKeyname) {
skippedAmount += 1
continue
}
handledKeys = append(handledKeys, parsedKeyname)
packageAmount += 1
}
}
skippedAmount = 0
handledKeys = []string{}
packageWg := sync.WaitGroup{}
packageKeys := make(chan CacheKeyData, packageAmount)
totalCount := 0
for _, curPackage := range sensorDetails.CodeScanner {
if totalCount >= maxSoftwareAmount {
log.Printf("[WARNING] Reached max amount of software+packages to update for sensor '%s'. Total count: %d. Skipped amount: %d", sensorDetails.Hostname, totalCount, skippedAmount)
break
}
// Loop the inner part
for _, software := range curPackage.Packages {
if totalCount >= maxSoftwareAmount {
break
}
//parsedKeyname := fmt.Sprintf("%s_%s", strings.TrimSpace(strings.ReplaceAll(strings.ToLower(software.Name), " ", "_")), sensorDetails.OS)
parsedKeyname := strings.TrimSpace(strings.ReplaceAll(strings.ToLower(software.Name), " ", "_"))
if ArrayContains(handledKeys, parsedKeyname) {
skippedAmount += 1
continue
}
handledKeys = append(handledKeys, parsedKeyname)
packageWg.Add(1)
go func(parsedKeyname string, software Software) {
defer packageWg.Done()
// 1. Get existing key
// 2. Update Versions & Hostnames
// 3. If it existed already, don't update the "Last Seen" field (or set it to the oldest of the two)
software.OS = curPackage.Type
software.Hostnames = []HostDetails{
HostDetails{
Hostname: sensorDetails.Hostname,
Version: software.Version,
UpdatedAt: time.Now().Unix(),
Paths: []string{curPackage.Path},
},
}
if len(software.Version) > 0 {
software.Versions = []string{software.Version}
}
datastoreId := fmt.Sprintf("%s_%s_%s", orborusDetails.OrgId, parsedKeyname, datastorePackageIndex)
config, getCacheError := GetDatastoreKey(ctx, datastoreId, datastorePackageIndex)
if getCacheError != nil {
//log.Printf("[ERROR] Failed to get existing datastore key for software '%s': %s", parsedKeyname, getCacheError)
} else if len(config.Value) > 0 {
unmarshalledSoftware := Software{}
err := json.Unmarshal([]byte(config.Value), &unmarshalledSoftware)
if err == nil {
hostPathExists := false
versionExists := false
for foundHostIndex, foundHost := range unmarshalledSoftware.Hostnames {
if foundHost.Hostname == sensorDetails.Hostname && foundHost.Version == software.Version {
unmarshalledSoftware.Hostnames[foundHostIndex].UpdatedAt = time.Now().Unix()
found := false
for _, path := range unmarshalledSoftware.Hostnames[foundHostIndex].Paths {
if path == curPackage.Path {
unmarshalledSoftware.Hostnames[foundHostIndex].Paths = append(unmarshalledSoftware.Hostnames[foundHostIndex].Paths, path)
found = true
break
}
}
if !found {
hostPathExists = true
}
break
}
}
if !hostPathExists {
unmarshalledSoftware.Hostnames = append(unmarshalledSoftware.Hostnames, HostDetails{
Hostname: sensorDetails.Hostname,
Version: software.Version,
UpdatedAt: time.Now().Unix(),
Paths: []string{curPackage.Path},
})
}
if ArrayContains(unmarshalledSoftware.Versions, software.Version) {
versionExists = true
} else {
unmarshalledSoftware.Versions = append(unmarshalledSoftware.Versions, software.Version)
}
if hostPathExists && versionExists {
if debug {
//log.Printf("[DEBUG] Package '%s' on host '%s' with version '%s' already exists in datastore. Skipping update.", software.Name, sensorDetails.Hostname, software.Version)
}
packageKeys <- CacheKeyData{
Key: "",
}
return
}
software = unmarshalledSoftware
}
}
software.Version = ""
parsedValue, err := json.Marshal(software)
if err != nil {
log.Printf("[ERROR] Failed to marshal Package for datastore update: %s. Software: %#v", err, curPackage)
packageKeys <- CacheKeyData{
Key: "",
}
return
}
packageKeys <- CacheKeyData{
Key: parsedKeyname,
Category: datastorePackageIndex,
Value: string(parsedValue),
OrgId: orborusDetails.OrgId,
}
}(parsedKeyname, software)
totalCount += 1
}
}
softwareWg.Wait()
close(softwareKeys)
packageWg.Wait()
close(packageKeys)
// Doing another dedup here as well
newPackageArray := []CacheKeyData{}
for key := range packageKeys {
if key.Key == "" {
continue
}
found := false
for packageIndex, newPackage := range newPackageArray {
if newPackage.Key != key.Key {
continue
}
log.Printf("[DEBUG] FOUND DUPE: %s", key.Key)
found = true
unmarshalledSoftwareNew := Software{}
err := json.Unmarshal([]byte(key.Value), &unmarshalledSoftwareNew)
if err != nil {
log.Printf("[ERROR] Failed to unmarshal software for package deduplication: %s. Software: %#v", err, key.Value)
continue
}
unmarshalledSoftwareExisting := Software{}
err = json.Unmarshal([]byte(newPackage.Value), &unmarshalledSoftwareExisting)
if err != nil {
log.Printf("[ERROR] Failed to unmarshal software for package deduplication: %s. Software: %#v", err, newPackage.Value)
continue
}
// Make sure the path and version exists
updated := false
for _, newHost := range unmarshalledSoftwareNew.Hostnames {
if !ArrayContains(unmarshalledSoftwareExisting.Versions, newHost.Version) {
continue
}
existingHostIndex := -1
for i, existingHost := range unmarshalledSoftwareExisting.Hostnames {
if existingHost.Hostname == newHost.Hostname {
existingHostIndex = i
break
}
}
if existingHostIndex == -1 {
unmarshalledSoftwareExisting.Hostnames = append(unmarshalledSoftwareExisting.Hostnames, newHost)
} else {
for _, newPath := range newHost.Paths {
if !ArrayContains(unmarshalledSoftwareExisting.Hostnames[existingHostIndex].Paths, newPath) {
unmarshalledSoftwareExisting.Hostnames[existingHostIndex].Paths = append(unmarshalledSoftwareExisting.Hostnames[existingHostIndex].Paths, newPath)
updated = true
}
}
}
// Update the "Last Seen" field to be the oldest of the two
if unmarshalledSoftwareExisting.Hostnames[existingHostIndex].UpdatedAt < newHost.UpdatedAt {
unmarshalledSoftwareExisting.Hostnames[existingHostIndex].UpdatedAt = newHost.UpdatedAt
updated = true
}
}
// FIXME: SOMETHING is wrong here.
if updated {
if debug {
log.Printf("FOUND DUPE: %s. Updated existing package key with new host and paths. %#v", key.Key, unmarshalledSoftwareExisting)
log.Printf("Old value: %#v", newPackage.Value)
log.Printf("New value: %#v", key.Value)
}
parsedValue, err := json.Marshal(unmarshalledSoftwareExisting)
if err != nil {
log.Printf("[ERROR] Failed to marshal software for package deduplication update: %s. Software: %#v", err, unmarshalledSoftwareExisting)
continue
}
newPackageArray[packageIndex].Value = string(parsedValue)
}
}
// We need to deduplicate here
if !found {
newPackageArray = append(newPackageArray, key)
}
}
newSoftwareArray := []CacheKeyData{}
for key := range softwareKeys {
if key.Key == "" {
continue
}
newSoftwareArray = append(newSoftwareArray, key)
}
if debug {
log.Printf("[DEBUG] %s - Packages: %d. Software: %d. Skipped amount: %d", sensorDetails.Hostname, len(newPackageArray), len(newSoftwareArray), skippedAmount)
}
if len(newSoftwareArray) > 0 {
if debug {
log.Printf("[DEBUG] Updating datastore with %d software keys for sensor '%s'", len(newSoftwareArray), sensorDetails.Hostname)
}
// Set them in the datastore (with some delay to avoid spikes)
_, err = SetDatastoreKeyBulk(ctx, newSoftwareArray)
if err != nil {
log.Printf("[ERROR] Failed to update datastore with %d software keys for sensor '%s': %s", len(newSoftwareArray), sensorDetails.Hostname, err)
}
}
if len(newPackageArray) > 0 {
if debug {
log.Printf("[DEBUG] Updating datastore with %d package keys for sensor '%s'", len(newPackageArray), sensorDetails.Hostname)
}
// Set them in the datastore (with some delay to avoid spikes)
_, err = SetDatastoreKeyBulk(ctx, newPackageArray)
if err != nil {
log.Printf("[ERROR] Failed to update datastore with %d package keys for sensor '%s': %s", len(newPackageArray), sensorDetails.Hostname, err)
}
}
// Loading in historical info
// Putting it here so we don't re-upload without a reason
if len(sensorDetails.CodeScanner) == 0 || len(sensorDetails.CodeScanner) == 0 {
datastoreId := fmt.Sprintf("%s_%s_%s", orborusDetails.OrgId, parsedHostname, datastoreSensorIndex)
cachedHost, err := GetDatastoreKey(ctx, datastoreId, datastoreSensorIndex)
if err == nil && len(cachedHost.Value) > 0 {
// unmarshal value to check what exists
oldHost := SensorDetails{}
err := json.Unmarshal([]byte(cachedHost.Value), &oldHost)
if err == nil {
if len(oldHost.User) > 0 && len(sensorDetails.User) == 0 {
sensorDetails.User = oldHost.User
}
if len(oldHost.CodeScanner) > 0 {
sensorDetails.CodeScanner = oldHost.CodeScanner
}
if len(oldHost.InstalledSoftware) > 0 {
sensorDetails.InstalledSoftware = oldHost.InstalledSoftware
}
}
} else {
log.Printf("[ERROR] Failed to load existing sensor details for sensor '%s' from datastore: %s", sensorDetails.Hostname, err)
}
}
sensorDetails.Checkin = time.Now().Unix()
hostData, err := json.Marshal(sensorDetails)
if err != nil {
log.Printf("[ERROR] Failed to marshal sensor details for datastore update for sensor '%s': %s", sensorDetails.Hostname, err)
} else {
hostKey := CacheKeyData{
Key: parsedHostname,
Category: datastoreSensorIndex,
Value: string(hostData),
OrgId: orborusDetails.OrgId,
}
// Set them in the datastore (with some delay to avoid spikes)
_, err = SetDatastoreKeyBulk(ctx, []CacheKeyData{hostKey})
if err != nil {
log.Printf("[ERROR] Failed to update datastore with software keys for sensor '%s': %s", sensorDetails.Hostname, err)
}
}
}
// Download handler for Orborus agent installation script. This is used in the "Assets" page for Orborus, and can be used by customers to easily install Orborus on their hosts. It returns a bash script that can be run on the target host to install Orborus with the correct configuration.
func GetOrborusDownloadCommand(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
// 1. fetch config in Go (no jq dependency)
c := OrborusDownloadConfig{
BaseURL: "https://shuffler.io",
Queue: "default",
Auth: "cb5st3d3Z!3X3zaJ*Pc",
OrgID: "",
SoftwareListEnabled: true,
HDEncryptedCheck: true,
ScreenlockCheck: true,
ResponseActions: "full",
AsRoot: true,
// Used for builder in dynamic scripts
BinaryBaseURL: "https://github.com/Shuffle/orborus/releases/latest/download",
Binaries: map[string]string{
"linux_amd64": "https://github.com/Shuffle/orborus/releases/latest/download/orborus-agent-linux-amd64",
"linux_arm64": "https://github.com/Shuffle/orborus/releases/latest/download/orborus-agent-linux-arm64",
"darwin_amd64": "https://github.com/Shuffle/orborus/releases/latest/download/orborus-agent-darwin-amd64",
"darwin_arm64": "https://github.com/Shuffle/orborus/releases/latest/download/orborus-agent-darwin-arm64",
"windows_amd64": "https://github.com/Shuffle/orborus/releases/latest/download/orborus-agent-windows-amd64.exe",
"windows_arm64": "https://github.com/Shuffle/orborus/releases/latest/download/orborus-agent-windows-arm64.exe",
},
}
// 2. URL overrides (optional)
q := r.URL.Query()
isWindows := false
if v := q.Get("os"); v != "" {
if v == "windows" {
isWindows = true
}
}
if v := q.Get("base_url"); v != "" {
c.BaseURL = v
}
if v := q.Get("queue"); v != "" {
c.Queue = v
}
if v := q.Get("auth"); v != "" {
c.Auth = v
}
if v := q.Get("org_id"); v != "" {
c.OrgID = v
}
if v := q.Get("response_actions"); v != "" {
c.ResponseActions = v
}
if v := q.Get("log_forwarding"); v != "" {
c.LogForwarding = v
}
if v := q.Get("software_list_enabled"); v != "" {
c.SoftwareListEnabled = v == "true"
}
if v := q.Get("hd_encrypted_check"); v != "" {
c.HDEncryptedCheck = v == "true"
}
if v := q.Get("screenlock_check"); v != "" {
c.ScreenlockCheck = v == "true"
}
if v := q.Get("admin"); v != "" {
c.AsRoot = v != "false"
}
// Check the "AUTH" header for a secret value to allow overriding the config (for security)
if authHeader := r.Header.Get("AUTH"); authHeader != "" {
c.Auth = authHeader
}
// Quite untested.
script := ""
if isWindows {
script = fmt.Sprintf(`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Start-Process powershell -Verb RunAs -ArgumentList @(
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"iwr <your-url> | iex"
)
Write-Host "started sensor installation in a new elevated PowerShell window. Please follow the prompts there to complete installation.",
exit 1
}
$ErrorActionPreference = "Stop"
# ===== injected config (from cfg) =====
$BASE_URL = "%s"
$QUEUE = "%s"
$AUTH = "%s"
$ORG_ID = "%s"
$SOFTWARE_LIST_ENABLED = "%t"
$CODE_SCANNER_ENABLED = "%t"
$HD_ENCRYPTED_CHECK = "%t"
$SCREENLOCK_CHECK = "%t"
$RESPONSE_ACTIONS = "%s"
$LOG_FORWARDING = "%s"
# ===== install paths =====
$INSTALL_DIR = "$env:ProgramData\orborus"
New-Item -ItemType Directory -Force -Path $INSTALL_DIR | Out-Null
# ===== arch detection =====
if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") {
$ARCH = "amd64"
} elseif ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
$ARCH = "arm64"
} else {
Write-Error "Unsupported architecture: $env:PROCESSOR_ARCHITECTURE"
exit 1
}
$BIN_URL = "https://github.com/Shuffle/orborus/releases/latest/download/orborus-agent-windows-$ARCH.exe"
$BIN_PATH = Join-Path $INSTALL_DIR "orborus-agent.exe"
icacls $INSTALL_DIR /grant Users:R
icacls $BIN_PATH /grant Users:RX
# Remove if exists early as the process may be running and we need to change the file
$SERVICE_NAME = "orborus-agent"
echo "Deleting old sensor"
schtasks /Delete /TN $SERVICE_NAME /F
$p = Get-Process -Name "orborus-agent" -ErrorAction SilentlyContinue
if ($p) {
$p | Stop-Process -Force
}
Start-Sleep -Seconds 2
Write-Host "Downloading binary from $BIN_URL to $BIN_PATH..."
try {
Write-Host "Downloading via BITS..."
Start-BitsTransfer -Source $BIN_URL -Destination $BIN_PATH -ErrorAction Stop
}
catch {
Write-Host "BITS failed, falling back to Invoke-WebRequest..."
Invoke-WebRequest -Uri $BIN_URL -OutFile $BIN_PATH -UseBasicParsing -MaximumRedirection 10
}
icacls $INSTALL_DIR /grant Users:R
icacls $BIN_PATH /grant Users:RX
# ===== service =====
function Escape-ArgValue($v) {
if ($v -match "\s") {
return '"' + $v + '"'
}
return $v
}
$ARGS = @()
$ARGS += "--sensor_mode=true"
if ($BASE_URL) { $ARGS += "--base_url=$BASE_URL" }
if ($QUEUE) { $ARGS += "--queue=$QUEUE" }
if ($AUTH) { $ARGS += "--auth=$AUTH" }
if ($ORG_ID) { $ARGS += "--org_id=$ORG_ID" }
# Removed as they made the command more than 260 characters (hard limit)
# These are now being enabled by default.
#if ($SOFTWARE_LIST_ENABLED -eq "true") { $ARGS += "--software_list_enabled=true" }
#if ($SOFTWARE_LIST_ENABLED -eq "false") { $ARGS += "--software_list_enabled=false" }
#if ($HD_ENCRYPTED_CHECK -eq "true") { $ARGS += "--hd_encrypted_check=true" }
#if ($SCREENLOCK_CHECK -eq "true") { $ARGS += "--screenlock_check=true" }
if ($RESPONSE_ACTIONS) { $ARGS += "--response_actions=$RESPONSE_ACTIONS" }
if ($LOG_FORWARDING) { $ARGS += "--log_forwarding=$LOG_FORWARDING" }
for ($i = 0; $i -lt $ARGS.Count; $i++) {
if ($ARGS[$i] -match '=') {
$parts = $ARGS[$i] -split '=', 2
$key = $parts[0]
$val = $parts[1]
if ($val -match '\s' -and $val -notmatch '^".*"$') {
$val = '"' + $val + '"'
}
$ARGS[$i] = "$key=$val"
}
}
$ARGS = $ARGS -join " "
# ===== create service =====
$WRAPPER = Join-Path $INSTALL_DIR "run-orborus.bat"
## Give exec permissions as user
icacls $WRAPPER /grant Users:RX
echo "Writing bat file to $WRAPPER"
$writer = New-Item -ItemType File -Path $Wrapper -Force
# Pre-prep
$line2 = "cd /d " + '"' + $INSTALL_DIR + '"'
$line4 = 'start "" ' + '"' + $BIN_PATH + '"' + " " + $ARGS + " >> orborus.log 2>&1"
Add-Content $WRAPPER "@echo off"
Add-Content $WRAPPER $line2
Add-Content $WRAPPER "echo STARTED >> debug.log"
Add-Content $WRAPPER $line4
Add-Content $WRAPPER "echo EXIT CODE %sRRORLEVEL%s >> debug.log"
echo "Starting scheduled task"
$PARSED_WRAPPER = '"' + $WRAPPER + '"'
# IF you want to run it without admin permissions, don't set /RU SYSTEM here
# Problem is then it's controllable by users too. That's fine for now.
$RUN_AS_ROOT = "%t"
if ($RUN_AS_ROOT -eq "true") {
echo "Running as root (default) - admin=false to disable"
schtasks /Create /TN $SERVICE_NAME /TR "$PARSED_WRAPPER" /SC ONSTART /RU "SYSTEM" /RL HIGHEST /F
} else {
echo "Running as normal $env:USERNAME"
schtasks /Create /TN $SERVICE_NAME /TR "$PARSED_WRAPPER" /SC ONSTART /RL HIGHEST /F
}
echo "Running service"
schtasks /Run /TN $SERVICE_NAME
Write-Host "orborus-agent installed"`,
c.BaseURL,
c.Queue,
c.Auth,
c.OrgID,
c.SoftwareListEnabled,
c.CodeScannerEnabled,
c.HDEncryptedCheck,
c.ScreenlockCheck,
c.ResponseActions,
c.LogForwarding,
"%E",
"%",
c.AsRoot,
)
} else {
script = fmt.Sprintf(`#!/usr/bin/env bash
set -e
#if [ "$(id -u)" -ne 0 ]; then
# echo "Run installer as root."
# exit 1
#fi
# =========================
# Detect OS + ARCH
# =========================
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$ARCH" in
x86_64|amd64) ARCH="amd64" ;;
arm64|aarch64) ARCH="arm64" ;;
*)
echo "unsupported architecture: $ARCH"
exit 1
;;
esac
if [[ "$OS" != "linux" && "$OS" != "darwin" ]]; then
echo "unsupported OS: $OS"
exit 1
fi
echo ""
echo "Download started. Please be patient while we install the sensor. Detected OS: $OS, ARCH: $ARCH"
echo ""
# =========================
# Config (from Go injection)
# =========================
BASE_URL="%s"
QUEUE="%s"
AUTH="%s"
ORG_ID="%s"
SOFTWARE_LIST_ENABLED="%t"
CODE_SCANNER_ENABLED="%t"
HD_ENCRYPTED_CHECK="%t"
SCREENLOCK_CHECK="%t"
RESPONSE_ACTIONS="%s"
LOG_FORWARDING="%s"
# =========================
# Binary selection
# =========================
BIN_BASE="%s"
BIN_URL="${BIN_BASE}/orborus-agent-${OS}-${ARCH}"
# =========================
# Install binary
# =========================
INSTALL_PATH="/usr/local/bin/orborus"
echo "Download starting from $BIN_URL... This may take a minute."
curl -fsSL "$BIN_URL" -o /tmp/orborus
chmod +x /tmp/orborus
echo "If prompted, please input your sudo password to allow installation (required for service setup and sensor capabilities). Contact support@shuffler.io if you need help."
sudo mv /tmp/orborus "$INSTALL_PATH"
echo "Installed binary to $INSTALL_PATH"
# =========================
# Linux service (systemd)
# =========================
install_linux() {
sudo tee /etc/systemd/system/orborus.service > /dev/null <<EOF
[Unit]
Description=Orborus Agent
After=network.target
[Service]
Type=simple
ExecStart=$INSTALL_PATH \
--sensor_mode=true \
--base_url=$BASE_URL \
--queue=$QUEUE \
--auth=$AUTH \
--org_id=$ORG_ID \
--software_list_enabled=$SOFTWARE_LIST_ENABLED \
--code_scanner_enabled=$CODE_SCANNER_ENABLED \
--hd_encrypted_check=$HD_ENCRYPTED_CHECK \
--screenlock_check=$SCREENLOCK_CHECK \
--log_forwarding=$LOG_FORWARDING \
--response_actions=$RESPONSE_ACTIONS
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable orborus
sudo systemctl restart orborus
}
# =========================
# macOS service (launchd)
# =========================
install_macos() {
PLIST=~/Library/LaunchAgents/com.orborus.agent.plist
mkdir -p ~/Library/LaunchAgents
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.orborus.agent</string>
<key>ProgramArguments</key>
<array>
<string>$INSTALL_PATH</string>
<string>--sensor_mode=true</string>
<string>--base_url=$BASE_URL</string>
<string>--queue=$QUEUE</string>
<string>--auth=$AUTH</string>
<string>--org_id=$ORG_ID</string>
<string>--software_list_enabled=$SOFTWARE_LIST_ENABLED</string>
<string>--code_scanner_enabled=$CODE_SCANNER_ENABLED</string>
<string>--hd_encrypted_check=$HD_ENCRYPTED_CHECK</string>
<string>--screenlock_check=$SCREENLOCK_CHECK</string>
<string>--log_forwarding=$LOG_FORWARDING</string>
<string>--response_actions=$RESPONSE_ACTIONS</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
EOF
launchctl unload "$PLIST" 2>/dev/null || true
launchctl load "$PLIST"
}
# =========================
# Execute
# =========================
if [ "$OS" = "linux" ]; then
install_linux
echo "orborus installed successfully"
elif [ "$OS" = "darwin" ]; then
install_macos
echo "orborus installed successfully"
fi
`,
c.BaseURL,
c.Queue,
c.Auth,
c.OrgID,
c.SoftwareListEnabled,
c.CodeScannerEnabled,
c.HDEncryptedCheck,
c.ScreenlockCheck,
c.ResponseActions,
c.LogForwarding,
c.BinaryBaseURL,
)
}
w.Write([]byte(script))
}