Suspicious Package

Automating with AppleScript

Suspicious Package is a scriptable application, which means that you can automate it using AppleScript.

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

If you're using AppleScript in any significant way, we highly recommend checking out the venerable Script Debugger application. While you can get by with the built-in Script Editor application, Script Debugger is more capable and better designed in every way — yes, it costs Actual Money, but it's well worth it.
In addition to AppleScript, there are other scripting dialects that are built upon the same Open Scripting Architecture (OSA) — which is, in turn, built upon the Apple Event technology that dates back to System 7 in the '90s.

JavaScript for Automation (JXA) was introduced back in OS X 10.10 (Yosemite), and allows you to use modern JavaScript for communicating with scriptable applications. On the plus side, you get the built-in capabilities of the JavaScript language, including all of the improvements of the last decade, because JXA is built on the same JavaScriptCore framework that underlies Safari. On the downside, JXA itself seems to have been largely neglected in the last decade — although it does still broadly work. Apple has done nothing with JXA documentation since OS X 10.11 (El Capitan), but this site by Christian Kirsch is a fantastic resource.

Alternatively, the Scripting Bridge was introduced back in Mac OS X 10.5 (Leopard), and allows you to use Objective-C for communicating with scriptable applications. For the shrinking number of us that still like Objective-C, the appeal is obvious. But the scripting bridge seems to be neglected about as much as JXA. The API is still described in the active documentation, but important conceptual information — such as how to generate bridging headers — is relegated to the archive.

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, and tell Suspicious Package to open it
var thePackage = Path( Standard.pathTo( 'desktop' ) + '/JavaForOSX.pkg' );
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' ) + '/sample/' + scriptName );
// clean up any old version that can cause this to fail; we use the ObjC bridge here,
// because using System Events delete() from JXA is maddeningly difficult.
$.NSFileManager.defaultManager.removeItemAtPathError( outFilePath.toString(), undefined );
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 exported symbols defined by 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("Symbol 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 symbol info by exporting the executable
set symbolSummary to my symbolsForExecutable(anExec)
set symbolStatus to "black" -- normal text color if okay
on error e number n
set symbolSummary to ("Error: " & e)
set symbolStatus to "red" -- show in red if any error
end try
-- add it to our report
my addToReport(theReport, POSIX path of anExec, symbolSummary, symbolStatus)
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 symbol info (as text) for given "installed item" of executable type
on symbolsForExecutable(execItem)
-- determine where to write temporary exported item
set tmpPath to POSIX path of (path to temporary items)
set exportPath to (tmpPath & (name of execItem))
-- clean up the old temporary files that can get in our way (see below)
tell application "System Events"
try
delete file exportPath
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 nm tool to extract the external defined symbols
return (do shell script "/usr/bin/nm -gU " & quoted form of exportPath)
end symbolsForExecutable
-- 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 application "TextEdit"
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 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( "Symbol 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 symbol info by exporting the executable
var anExec = mainExecs[ j ];
var symbolSummary;
var symbolStatus;
try
{
symbolSummary = symbolsForExecutable( anExec );
symbolStatus = 'black'; // normal text color if okay
}
catch ( e )
{
symbolSummary = 'Error: ' + e.toString();
symbolStatus = 'red'; // show in red if any error
}
// add it to our report
addToReport( theReport, anExec.posixPath(), symbolSummary, symbolStatus );
}
}
}
TextEdit.activate();
}
else
{
Standard.displayDialog( "Didn't find any app or XPC executables in package" );
}
// get the symbol info (as text) for given "installed item" of executable type
function symbolsForExecutable( execItem )
{
// determine where to write temporary exported item
var tmpPath = Standard.pathTo( 'temporary items' ) + '/sample';
var exportPath = Path( tmpPath + '/' + execItem.name() );
// 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 );
// 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 nm tool to extract the external defined symbols
return Standard.doShellScript( '/usr/bin/nm -gU "' + exportPath + '"' );
}
// 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.