// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.
//go:build darwin
// +build darwin
// Package application contains application gatherer.
package application
import (
"encoding/xml"
"fmt"
"os/exec"
"strconv"
"strings"
"time"
"github.com/aws/amazon-ssm-agent/agent/context"
"github.com/aws/amazon-ssm-agent/agent/log"
"github.com/aws/amazon-ssm-agent/agent/platform"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/model"
)
var (
systemProfilerCmd = "system_profiler"
xmlFormatArg = "-xml"
applicationDataArg = "SPApplicationsDataType"
appNameKey = "_name"
publisherKey = "obtained_from"
architectureKey = "runtime_environment"
versionKey = "version"
// if wanted to collect the information of additional agent packages, add
// the tag --pkgs="agent-package-name". We can add multiple --pkgs
// options to the pkgutil command. And even we can do pattern match with
// --pkgs options, following command will support (because of xargs)
pkgutilCmd = fmt.Sprintf(`pkgutil --pkgs=%s | \
xargs -n 1 pkgutil --pkg-info-plist | \
grep -v DOCTYPE | \
grep -v 'xml version="1.0" encoding="UTF-8"'`, amazonSsmAgentMac)
packageNameKey = "pkgid"
packageVersionKey = "pkg-version"
packageInsTimeKey = "install-time"
)
// decoupling exec.Command for easy testability
var cmdExecutor = executeCommand
func executeCommand(command string, args ...string) ([]byte, error) {
return exec.Command(command, args...).CombinedOutput()
}
func platformInfoProvider(log log.T) (name string, err error) {
return platform.PlatformName(log)
}
// collectPlatformDependentApplicationData collects all application data from the system using system_profiler command.
func collectPlatformDependentApplicationData(context context.T) (appData []model.ApplicationData) {
var err error
log := context.Log()
cmd := systemProfilerCmd
args := []string{xmlFormatArg, applicationDataArg}
if appData, err = getApplicationData(context, cmd, args); err != nil {
log.Info("system_profiler command failed!")
}
pkgData, err := getInstalledPackages(context, pkgutilCmd)
if err == nil {
var i int
for i = 0; i < len(pkgData); i++ {
appData = append(appData, pkgData[i])
}
} else {
log.Info("pkgutil command failed!")
}
return
}
func getInstalledPackages(context context.T, command string) (data []model.ApplicationData, err error) {
log := context.Log()
var output []byte
log.Debugf("Executing command: %v", command)
if output, err = cmdExecutor("bash", "-c", command); err != nil {
log.Errorf("Failed to execute command : %v with error - %v",
command,
err.Error())
log.Debugf("Command Stderr: %v", string(output))
err = fmt.Errorf("Command failed with error: %v", string(output))
} else {
cmdOutput := string(output)
modifiedCmdOutput := "" + cmdOutput + ""
if data, err = convertToApplicationDataFromInstalledPkg(modifiedCmdOutput); err != nil {
err = fmt.Errorf("Unable to convert installed Packages to ApplicationData - %v", err.Error())
log.Errorf(err.Error())
} else {
log.Infof("Number of packages detected - %v", len(data))
}
}
return
}
func convertToApplicationDataFromInstalledPkg(input string) (data []model.ApplicationData, err error) {
// Application type struct to hold the app xml string
type Application struct {
App string `xml:",innerxml"`
}
// Applications type struct to hold array of Application at path
// plist>dict
type Applications struct {
Apps []Application `xml:"plist>dict"`
}
commandOutput := Applications{Apps: []Application{}}
err = xml.Unmarshal([]byte(input), &commandOutput)
if err != nil {
return
}
// Loop over the Applications data, and create the output data
var i int
for i = 0; i < len(commandOutput.Apps); i++ {
appName := getFieldValue(commandOutput.Apps[i].App, packageNameKey, "string")
// Convert Unix timestamp to DateTime
packageInstalledTime := getFieldValue(commandOutput.Apps[i].App, packageInsTimeKey, "integer")
packageInstalledTimeToInteger, errParseInt := strconv.ParseInt(packageInstalledTime, 10, 64)
var installedDateTime = ""
if errParseInt == nil {
tm := time.Unix(packageInstalledTimeToInteger, 0).UTC()
installedDateTime = tm.Format(time.RFC3339)
}
itemContent := model.ApplicationData{
Name: appName,
ApplicationType: "",
Publisher: "",
Version: getFieldValue(commandOutput.Apps[i].App, packageVersionKey, "string"),
InstalledTime: installedDateTime,
Architecture: "",
URL: "",
Summary: "",
PackageId: "",
Release: "",
Epoch: "",
CompType: componentType(appName),
}
data = append(data, itemContent)
}
return
}
// getApplicationData runs a terminal command and gets information about all packages/applications
func getApplicationData(context context.T, command string, args []string) (data []model.ApplicationData, err error) {
var output []byte
log := context.Log()
log.Debugf("Executing command: %v %v", command, args)
if output, err = cmdExecutor(command, args...); err != nil {
log.Errorf("Failed to execute command : %v %v with error - %v",
command,
args,
err.Error())
log.Debugf("Command Stderr: %v", string(output))
err = fmt.Errorf("Command failed with error: %v", string(output))
} else {
cmdOutput := string(output)
log.Debugf("Command output: %v", cmdOutput)
if data, err = convertToApplicationData(cmdOutput); err != nil {
err = fmt.Errorf("Unable to convert query output to ApplicationData - %v", err.Error())
log.Errorf(err.Error())
} else {
log.Infof("Number of applications detected - %v", len(data))
}
}
return
}
// convert command output to XML (deserialize)
// Get the application data from xpath array>dict>array>dict
// parse the Application data, and the get the value for the respective keys
func convertToApplicationData(input string) (data []model.ApplicationData, err error) {
/*
Sample Applications Data
_SPCommandLineArguments
/usr/sbin/system_profiler
-nospawn
-xml
SPApplicationsDataType
-detailLevel
full
_SPCompletionInterval
2.8108129501342773
_SPResponseTime
2.9170479774475098
_dataType
SPApplicationsDataType
_detailLevel
1
_items
_name
Calendar
has64BitIntelCode
yes
lastModified
2019-04-03T07:20:22Z
obtained_from
apple
path
/Applications/Calendar.app
runtime_environment
arch_x86
signed_by
Software Signing
Apple Code Signing Certification Authority
Apple Root CA
version
11.0
_name
Amazon Chime
has64BitIntelCode
yes
lastModified
2020-02-06T22:52:21Z
obtained_from
identified_developer
path
/Applications/Amazon Chime.app
runtime_environment
arch_x86
signed_by
Developer ID Application: AMZN Mobile LLC (94KV3E626L)
Developer ID Certification Authority
Apple Root CA
version
4.28.7255
*/
// Application type struct to hold the app xml string
type Application struct {
App string `xml:",innerxml"`
}
// Applications type struct to hold array of Application at path array>dict>array>dict
type Applications struct {
Apps []Application `xml:"array>dict>array>dict"`
}
commandOutput := Applications{Apps: []Application{}}
err = xml.Unmarshal([]byte(input), &commandOutput)
if err != nil {
return
}
// Loop over the Applications data, and create the output data
var i int
for i = 0; i < len(commandOutput.Apps); i++ {
appName := getFieldValue(commandOutput.Apps[i].App, appNameKey, "string")
itemContent := model.ApplicationData{
Name: appName,
ApplicationType: "",
Publisher: getFieldValue(commandOutput.Apps[i].App, publisherKey, "string"),
Version: getFieldValue(commandOutput.Apps[i].App, versionKey, "string"),
InstalledTime: "",
Architecture: getFieldValue(commandOutput.Apps[i].App, architectureKey, "string"),
URL: "",
Summary: "",
PackageId: "",
Release: "",
Epoch: "",
CompType: componentType(appName),
}
data = append(data, itemContent)
}
return
}
// value of "key" is present as key in input string
// Next line of the above xml tag, value contains value
func getFieldValue(input string, key string, fieldValueTagName string) string {
/*
input string format
_name
Calendar
has64BitIntelCode
yes
lastModified
2019-04-03T07:20:22Z
obtained_from
apple
path
/Applications/Calendar.app
runtime_environment
arch_x86
signed_by
Software Signing
Apple Code Signing Certification Authority
Apple Root CA
version
11.0
*/
var keyItem = "" + key + ""
keyStartPos := strings.Index(input, keyItem)
if keyStartPos < 0 {
return ""
}
var valueStartTag = `<` + fieldValueTagName + `>`
var valueEndTag = `` + fieldValueTagName + `>`
afterKeyStr := input[keyStartPos+len(keyItem):]
nextTagStartPos := strings.Index(afterKeyStr, "<")
nextStringTagPos := strings.Index(afterKeyStr, valueStartTag) // "")
valueStartPos := nextStringTagPos + len(valueStartTag) // "")
return strings.TrimSpace(afterKeyStr[valueStartPos:nextEndStringTagPos])
}