Suspicious Package

Automating with AppleScript

Suspicious Package is a scriptable application, which means that you can automate it using AppleScript (or one of the newer and less-documented scripting dialects, such as JavaScript for Automation or the Scripting Bridge for Objective-C).

The Suspicious Package scripting dictionary is thoroughly documented, and contains many bits of sample code. You can open the scripting dictionary from Script Editor, of course, or from within Suspicious Package, a shortcut is to use Help > Open Scripting Dictionary. the Suspicious Package scripting dictionary

A few examples of how to use Suspicious Package via AppleScript follow. (We've also translated them into the JavaScript for Automation (JXA) dialect; use the buttons at the top of each example to switch between languages.)

This first script demonstrates how to open a package — fairly standard AppleScript for targeting a Cocoa application — and how to get at installed files and folders by name. This also demonstrates how you can use the reveal command to show an installed item in a new tab in the Suspicious Package UI:

-- get path to package
set thePackage to (get path to desktop as string) & "JavaForOSX.pkg"
tell application "Suspicious Package"
-- tell Suspicious Package to open the package
set theDocument to (open file thePackage)
-- find any launchd agent plist directory, by partial POSIX path
set launchAgents to (get installed item "System/Library/LaunchAgents" of theDocument)
-- does the package install to the launchd agent directory?
if exists launchAgents then
-- examine each launch agent plist in the package
repeat with anAgent in installed items of launchAgents
-- get some properties of the plist
display notification "Found " & (name of anAgent) & " with owner " & (owner of anAgent)
log (get URL of anAgent)
-- reveal the plist in a new tab in the Suspicious Package UI
reveal anAgent
end repeat
end if
-- close the package
close theDocument
end tell
// get access to Standard Additions
var Standard = Application.currentApplication();
Standard.includeStandardAdditions = true;
// get access to Suspicious Package
var SuspiciousPackage = Application( 'Suspicious Package' );
// get path to package
var thePackage = Path( Standard.pathTo( 'desktop' ) + '/JavaForOSX.pkg' );
// tell it to open the package
var theDocument = SuspiciousPackage.open( thePackage );
// find any launchd agent plist directory, by partial POSIX path
var launchAgentDirectory = theDocument.installedItems.byName( 'System/Library/LaunchAgents' );
// does the package install to the launchd agent directory?
if ( launchAgentDirectory )
{
// examine each launch agent plist in the package
var launchAgents = launchAgentDirectory.installedItems;
for ( var i = 0 ; i < launchAgents.length ; ++i )
{
var anAgent = launchAgents[ i ];
// get some properties of the plist
Standard.displayNotification( 'Found ' + anAgent.name() + ' with owner ' + anAgent.owner() );
console.log( anAgent.url() );
// reveal the plist in a new tab in the Suspicious Package UI
SuspiciousPackage.reveal( anAgent );
}
}
// close the package
theDocument.close();

Suspicious Package also provides a find command, which can be used to efficiently locate installed files using a standard AppleScript whose clause:

tell application "Suspicious Package"
-- find all installed .app bundles underneath the System folder
set systemApps to (find bundles under installed item "/System" of theDocument whose name ends with ".app")
-- did we find anything?
if (count of systemApps) is not 0 then
-- reveal each app in Suspicious Package
repeat with anApp in systemApps
reveal anApp
end repeat
end if
end tell
// find all installed .app bundles underneath the System folder
var searchFolder = theDocument.installedItems.byName( 'System' );
var systemApps = SuspiciousPackage.find( 'bundles',
{
// note that whose() must be called on an element array, not an
// individual ObjectSpecifier, so we target the contained items
under: searchFolder.installedItems.whose( {
name: { _endsWith: '.app' },
} )
} );
// if we found anything, reveal each app in Suspicious Package
for ( var i = 0 ; i < systemApps.length ; ++i )
SuspiciousPackage.reveal( systemApps[ i ] );

You can also use uniform type identifiers (UTIs) to look for related file kinds. Here, we check an item for conformance to com.apple.property-list, which means that any variant on a property list will match:

tell application "Suspicious Package"
-- examine each of the items under CoreServices
repeat with coreService in (installed items of installed item "/System/Library/CoreServices" of theDocument)
-- get the UTI of the item
set theUTI to (UTI of coreService)
-- if the item's UTI is a property list or something more specific, show it
if theUTI conforms to "com.apple.property-list" then
reveal coreService
end if
end repeat
end tell
// examine each of the items under CoreServices
var coreServices = theDocument.installedItems.byName( '/System/Library/CoreServices' ).installedItems;
for ( var i = 0 ; i < coreServices.length ; ++i )
{
// get the UTI of the item
var theUTI = coreServices[ i ].uti;
// if the item's UTI is a property list or something more specific, show it
if ( SuspiciousPackage.conforms( theUTI, { to: 'com.apple.property-list' } ) )
SuspiciousPackage.reveal( coreServices[ i ] );
}

UTI conformance can also be used within a find command:

tell application "Suspicious Package"
-- make UTI object for generic public.font, if not already known in the package
if not (uniform type ID "public.font" of theDocument exists) then
tell theDocument to make new uniform type ID with properties {name:"public.font"}
end if
-- fetch the UTI object for generic public.font
set fontType to (uniform type ID "public.font" of theDocument)
-- use public.font to do a find command: "UTI is in" is overloaded to mean "UTI conforms to"
set allFonts to (find content under installed items of theDocument where UTI is in fontType)
repeat with aFont in allFonts
log (get POSIX path of aFont)
end repeat
end tell
/*
Unfortunately, we haven't been able to make this example work in the JavaScript for
Automation dialect. The JXA "array filtering" syntax doesn't provide any way to express
an "is in" test, so we can't directly do the equivalent of this AppleScript:
where UTI is in fontType
JXA provides for a "contains" test, which is the reverse of "is in", but we've had no
luck getting this to work. It ought to be something like:
whose( { _match: [ fontType, { _contains: ObjectSpecifier().uti } ] } )
but JXA doesn't seem to like the first operand of a match not being a property on the
object being tested (i.e. an installedItem in this case). It only throws an obtuse error,
and never even sends an AppleEvent to Suspicious Package.
We've tried a dizzying number of variations to get this to work, with no luck.
*/

In addition to installed files, AppleScript can also be used to examine the install scripts, and to access the text or other content of each script. In this example, we either examine the text of the script for a particular string, or if the script is a binary executable, we use another app to open the raw binary data:

tell application "Suspicious Package"
-- get each of the install scripts in the package
repeat with aScript in installer scripts of theDocument
if aScript is not binary then
-- plain text script: look in the script text for a specific command
if (installer script text of aScript) contains "unlink" then
display dialog "Found unlink in " & name of aScript giving up after 1
end if
else if (UTI of aScript) conforms to "public.executable" then
-- binary script that is some sort of executable: get the raw binary data...
set scriptData to installer script data of aScript
if scriptData exists then
-- call our handler (below) to write the data to a temporary file
set tmpFile to (my writeScriptData(scriptData, (get short name of aScript)))
-- if we were able to write the temporary file, ask Hex Fiend to open it
if tmpFile exists then
tell application "Hex Fiend" to open file tmpFile
display notification "Opened " & (name of aScript) & " in Hex Fiend" with title "Suspicious Package Scripting"
end if
end if
end if
end repeat
end tell
-- handler to write the given script data to a temporary file with the given name
on writeScriptData(theData, scriptName)
set outFilePath to (path to temporary items as string) & scriptName
tell current application
try
set outFileNum to (open for access outFilePath with write permission)
on error number n
display dialog "Failed to open " & outFilePath & " for writing (error " & n & ")"
close access outFilePath
return
end try
try
write theData to outFileNum
set didWriteDataToFile to outFilePath
on error number n
display dialog "Failed to write data to " & outFilePath & "(error " & n & ")"
end try
close access outFileNum
end tell
return didWriteDataToFile
end writeScriptData
// get each of the install scripts in the package
var allScripts = theDocument.installerScripts;
for ( var i = 0 ; i < allScripts.length ; ++i )
{
var aScript = allScripts[ i ];
if ( ! aScript.binary() )
{
// plain text script: look in the script text for a specific command
if ( aScript.installerScriptText().indexOf( 'unlink' ) != -1 )
Standard.displayDialog( "Found unlink in " + aScript.name(), { givingUpAfter: 1 } );
}
else if ( SuspiciousPackage.conforms( aScript.uti, { to: 'public.executable' } ) )
{
// binary script that is some sort of executable: get the raw binary data...
var scriptData = aScript.installerScriptData();
if ( scriptData )
{
// call our function (below) to write the data to a temporary file
var tmpFile = writeScriptData( scriptData, aScript.shortName() );
// if we were able to write the temporary file, ask Xcode to open it
// (because JXA refuses to send an open event to Hex Fiend)
if ( tmpFile )
{
Application( 'Xcode' ).open( tmpFile );
Standard.displayNotification( 'Opened ' + aScript.name() + ' in Xcode',
{ withTitle: 'Suspicious Package' } );
}
}
}
}
// function to write the given script data to a temporary file with the given name
function writeScriptData( theData, scriptName )
{
var outFilePath = Path( Standard.pathTo( 'temporary items' ) + '/' + scriptName );
var outFileNum;
var didWriteDataToFile;
try
{
outFileNum = Standard.openForAccess( outFilePath, { writePermission: true } );
}
catch ( e )
{
Standard.displayDialog( "Failed to open " + outFilePath + " for writing (error: " + e + ")" );
Standard.closeAccess( outFilePath );
return;
}
try
{
Standard.write( theData, { to: outFileNum } );
didWriteDataToFile = outFilePath;
}
catch ( e )
{
Standard.displayDialog( "Failed to write data to " + outFilePath + " (error: " + e + ")" );
}
Standard.closeAccess( outFileNum );
return didWriteDataToFile;
}

Finally, in this example, we use the Suspicious Package export command to get the contents of certain executable files. Then, we use TextEdit — and some standard command-line tools — to create a report showing the entitlements associated with each executable:

tell application "Suspicious Package"
-- locate all of the .app and .xpc bundles installed by the package
set binaryBundles to (find bundles under installed items of theDocument whose name extension is "app" or name extension is "xpc")
-- did we find any?
if (count of binaryBundles) is not 0 then
-- create new TextEdit document to hold the report
set theReport to my makeReport("Entitlement Report for " & (get name of theDocument))
-- process each bundle
repeat with binaryBundle in binaryBundles
-- find the main executable folder for the bundle
set mainExecFolder to installed item "Contents/MacOS" of binaryBundle
if exists mainExecFolder then
-- process each executable in that folder (typically only one)
repeat with anExec in installed items of mainExecFolder
try
-- get the entitlement info by exporting the executable
set entitlementSummary to my entitlementsForExecutable(anExec)
set entitlementStatus to "black" -- normal text color if okay
on error e number n
set entitlementSummary to ("Error: " & e)
set entitlementStatus to "red" -- show in red if any error
end try
-- add it to our report
my addToReport(theReport, POSIX path of anExec, entitlementSummary, entitlementStatus)
end repeat
end if
end repeat
tell application "TextEdit" to activate
else
display dialog "Didn't find any app or XPC executables in package"
end if
end tell
-- handler to get the entitlement info (as text) for given "installed item" of executable type
on entitlementsForExecutable(execItem)
-- determine where to write temporary exported item and temporary entitlement plist
set tmpPath to POSIX path of (path to temporary items)
set exportPath to tmpPath & "/" & (name of execItem)
set blobPath to tmpPath & "/entitlements.blob"
set plistPath to tmpPath & "/entitlements.plist"
-- clean up the old temporary files that can get in our way (see below)
tell application "System Events"
try
delete file exportPath
end try
try
delete file blobPath
end try
end tell
-- ask Suspicious Package to export the executable item from the package; note that the
-- export command *won't* overwrite an existing file, so we delete any such above
tell application "Suspicious Package"
with timeout of 3000 seconds
export execItem to POSIX file exportPath
end timeout
end tell
-- use the standard codesign tool to extract the entitlements plist from the code signature;
-- note that --entitlements *won't* overwrite an existing file at blobPath (it won't even truncate
-- it to zero length), so we delete any such above, to avoid picking up wrong data below
do shell script "/usr/bin/codesign --display --entitlements=" & quoted form of blobPath & " " & quoted form of exportPath
-- check for no entitlements, in which case the extracted blob written by codesign will be
-- an empty file (provided the file didn't exist before running codesign, that is)
tell application "System Events"
set blobInfo to (get properties of file blobPath) -- so we can get size of the blob
if size of blobInfo is 0 then
error "No entitlements found (not sandboxed)"
end if
end tell
-- otherwise, use dd to strip off the 8 byte prefix, to make a valid XML plist
do shell script "/bin/dd if=" & quoted form of blobPath & " of=" & quoted form of plistPath & " bs=1 skip=8"
-- finally, use standard defaults tool to reformat the XML plist in a more succinct form
return (do shell script "/usr/bin/defaults read " & quoted form of plistPath)
end entitlementsForExecutable
-- handle to create report in TextEdit
on makeReport(title)
tell application "TextEdit"
set r to (make new document at front of documents)
tell r
make new paragraph at end of paragraphs with data title with properties {font:"Helvetica Neue Bold", size:18}
make new paragraph at end of paragraphs with data return & return
end tell
end tell
return r
end makeReport
-- handler to update report with one item
on addToReport(r, p, summary, status)
tell r
make new paragraph at end of paragraphs with data p with properties {font:"Menlo Bold", size:14}
make new paragraph at end of paragraphs with data return & return
make new paragraph at end of paragraphs with data summary with properties {font:"Menlo Regular", size:12, color:status}
make new paragraph at end of paragraphs with data return & return & return
end tell
end addToReport
// locate all of the .app and .xpc bundles installed by the package
var binaryBundles = SuspiciousPackage.find( 'bundles',
{
under: theDocument.installedItems.whose( {
_or:
[
{ nameExtension: 'app' },
{ nameExtension: 'xpc' },
]
} )
} );
// did we find any?
if ( binaryBundles.length != 0 )
{
// create new TextEdit document to hold the report
var TextEdit = Application( 'TextEdit' );
var theReport = makeReport( "Entitlement Report for " + theDocument.name() );
// process each bundle
for ( var i = 0 ; i < binaryBundles.length ; ++i )
{
// find the main executable folder for the bundle
var binaryBundle = binaryBundles[ i ];
var mainExecFolder = binaryBundle.installedItems.byName( 'Contents/MacOS' );
if ( mainExecFolder() )
{
// process each executable in that folder (typically only one)
var mainExecs = mainExecFolder.installedItems;
for ( var j = 0 ; j < mainExecs.length ; ++j )
{
// get the entitlement info by exporting the executable
var anExec = mainExecs[ j ];
var entitlementSummary;
var entitlementStatus;
try
{
entitlementSummary = entitlementsForExecutable( anExec );
entitlementStatus = 'black'; // normal text color if okay
}
catch ( e )
{
entitlementSummary = 'Error: ' + e.toString();
entitlementStatus = 'red'; // show in red if any error
}
// add it to our report
addToReport( theReport, anExec.posixPath(), entitlementSummary, entitlementStatus );
}
}
}
TextEdit.activate();
}
else
{
Standard.displayDialog( "Didn't find any app or XPC executables in package" );
}
// get the entitlement info (as text) for given "installed item" of executable type
function entitlementsForExecutable( execItem )
{
// determine where to write temporary exported item and temporary entitlement plist
var tmpPath = Standard.pathTo( 'temporary items' );
var exportPath = Path( tmpPath + '/' + execItem.name() );
var blobPath = Path( tmpPath + '/entitlements.blob' );
var plistPath = Path( tmpPath + '/entitlements.plist' );
// clean up the old temporary files that can get in our way (see below); we use the ObjC
// bridge here, because using System Events delete() from JXA is maddeningly difficult.
$.NSFileManager.defaultManager.removeItemAtPathError( exportPath.toString(), undefined );
$.NSFileManager.defaultManager.removeItemAtPathError( blobPath.toString(), undefined );
// ask Suspicious Package to export the executable item from the package; note that the
// export command *won't* overwrite an existing file, so we delete any such above
SuspiciousPackage.export( execItem, { to: exportPath }, { timeout: 3000 } );
// use the standard codesign tool to extract the entitlements plist from the code signature;
// note that --entitlements *won't* overwrite an existing file at blobPath (it won't even truncate
// it to zero length), so we delete any such above, to avoid picking up wrong data below
Standard.doShellScript( '/usr/bin/codesign --display --entitlements="' + blobPath + '" "' + exportPath + '"' );
// check for no entitlements, in which case the extracted blob written by codesign will be
// an empty file (provided the file didn't exist before running codesign, that is); again,
// we're using the ObjC bridge to avoid the awkwardness of System Events from JXA
if ( $.NSFileManager.defaultManager.attributesOfItemAtPathError( blobPath.toString(), undefined ).fileSize == 0 )
throw new Error( "No entitlements found (not sandboxed)" );
// otherwise, use dd to strip off the 8 byte prefix, to make a valid XML plist
Standard.doShellScript( '/bin/dd if="' + blobPath + '" of="' + plistPath + '" bs=1 skip=8' );
// finally, use standard defaults tool to reformat the XML plist in a more succinct form
return Standard.doShellScript( '/usr/bin/defaults read "' + plistPath + '"' );
}
// create report in TextEdit
function makeReport( title )
{
var r = TextEdit.Document().make();
var p = TextEdit.Paragraph( { font: 'Helvetica Neue Bold', size: 18 }, title );
r.paragraphs.push( p );
var p = TextEdit.Paragraph( {}, "\r\r" );
r.paragraphs.push( p );
return r;
}
// update report with one item
function addToReport( r, p, summary, stat )
{
var p = TextEdit.Paragraph( { font: 'Menlo Bold', size: 14 }, p );
r.paragraphs.push( p );
var p = TextEdit.Paragraph( {}, "\r\r" );
r.paragraphs.push( p );
var p = TextEdit.Paragraph( { font: 'Menlo Regular', size: 12, color: stat }, summary );
r.paragraphs.push( p );
var p = TextEdit.Paragraph( {}, "\r\r\r" );
r.paragraphs.push( p );
}

The above examples give a flavor of using AppleScript with Suspicious Package, but there are many other properties and elements you can use, including access to package signature information and any potential issues that were flagged for review. Again, check out the scripting dictionary for more information and many more examples.