Merge branch 'master' of github.com:facebook/osquery

This commit is contained in:
mike@arpaia.co 2014-11-12 10:57:14 -05:00
commit adb8bf7602
9 changed files with 372 additions and 291 deletions

View File

@ -6,12 +6,6 @@ if(APPLE)
set(OS_WHOLELINK_PRE "-Wl,-all_load")
set(OS_WHOLELINK_POST "")
else()
if(EXISTS "/etc/redhat-release")
set(CENTOS TRUE)
else()
set(UBUNTU TRUE)
endif()
set(OS_COMPILE_FLAGS "-std=c++11")
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(OS_COMPILE_FLAGS "${OS_COMPILE_FLAGS} -g")
@ -20,13 +14,17 @@ else()
set(OS_WHOLELINK_POST "-Wl,-no-whole-archive")
endif()
if(APPLE)
message("-- Building for OS X")
elseif(UBUNTU)
message("-- Building for Ubuntu")
elseif(CENTOS)
message("-- Building for CentOS")
endif()
# Use osquery language to set platform/os
execute_process(
COMMAND ${CMAKE_SOURCE_DIR}/tools/provision.sh get_platform
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE PLATFORM
OUTPUT_STRIP_TRAILING_WHITESPACE
)
string(REPLACE "." "_" PLATFORM "${PLATFORM}")
string(TOUPPER "${PLATFORM}" PLATFORM)
list(GET PLATFORM 0 OSQUERY_BUILD_OS)
list(GET PLATFORM 1 OSQUERY_BUILD_DISTRO)
# Make sure deps were built before compiling
execute_process(
@ -42,6 +40,16 @@ if(OSQUERY_DEPS_CHECK)
message(WARNING "${Esc}[31m${OSQUERY_DEPS_MESSAGE}${Esc}[m")
endif()
if(APPLE)
message("-- Building for OS X")
elseif(OSQUERY_BUILD_OS STREQUAL "UBUNTU")
set(UBUNTU TRUE)
message("-- Building for Ubuntu")
elseif(OSQUERY_BUILD_OS STREQUAL "CENTOS")
set(CENTOS TRUE)
message("-- Building for CentOS")
endif()
if(BUILD_SHARED_LIBS)
set(USER_COMPILE_FLAGS "-fPIC")
else()

View File

@ -90,7 +90,11 @@ execute_process(
OUTPUT_STRIP_TRAILING_WHITESPACE
)
ADD_DEFINITIONS("-DOSQUERY_BUILD_VERSION=${OSQUERY_BUILD_VERSION}")
ADD_DEFINITIONS("
-DOSQUERY_BUILD_VERSION=${OSQUERY_BUILD_VERSION}
-D${OSQUERY_BUILD_OS}=1
-D${OSQUERY_BUILD_OS}_${OSQUERY_BUILD_DISTRO}=1
")
MACRO(ADD_OSQUERY_LINK LINK)
list(APPEND OSQUERY_ADDITIONAL_LINKS ${LINK})

View File

@ -1,8 +1,8 @@
table_name("suid_bin")
schema([
Column(name="path", type="std::string"),
Column(name="unix_user", type="std::string"),
Column(name="unix_group", type="std::string"),
Column(name="username", type="std::string"),
Column(name="groupname", type="std::string"),
Column(name="permissions", type="std::string"),
])
implementation("suid_bin@genSuidBin")

View File

@ -1,27 +1,80 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#include <ctime>
#include <pwd.h>
#include <grp.h>
#include <sys/stat.h>
#include <boost/system/system_error.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/filesystem.hpp>
#include <glog/logging.h>
#include "osquery/database.h"
using std::string;
using boost::lexical_cast;
#include <boost/filesystem.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/system/system_error.hpp>
#include <glog/logging.h>
#include "osquery/database.h"
namespace osquery {
namespace tables {
QueryData genSuidBin() {
Row r;
QueryData results;
Status genBin(const boost::filesystem::path& path,
int perms,
QueryData& results) {
struct stat info;
// store user and group
if (stat(path.c_str(), &info) != 0) {
return Status(1, "stat failed");
}
// store path
Row r;
r["path"] = path.string();
struct passwd *pw = getpwuid(info.st_uid);
struct group *gr = getgrgid(info.st_gid);
// get user name + group
std::string user;
if (pw != nullptr) {
user = std::string(pw->pw_name);
} else {
user = boost::lexical_cast<std::string>(info.st_uid);
}
std::string group;
if (gr != nullptr) {
group = std::string(gr->gr_name);
} else {
group = boost::lexical_cast<std::string>(info.st_gid);
}
r["username"] = user;
r["groupname"] = group;
r["permissions"] = "";
if ((perms & 04000) == 04000) {
r["permissions"] += "S";
}
if ((perms & 02000) == 02000) {
r["permissions"] += "G";
}
results.push_back(r);
return Status(0, "OK");
}
QueryData genSuidBin() {
QueryData results;
boost::system::error_code error;
#if defined(UBUNTU)
// When building on supported Ubuntu systems, boost may ABRT.
if (geteuid() != 0) {
return results;
}
#endif
boost::filesystem::recursive_directory_iterator it =
boost::filesystem::recursive_directory_iterator(
boost::filesystem::path("/"), error);
@ -35,32 +88,10 @@ QueryData genSuidBin() {
while (it != end) {
boost::filesystem::path path = *it;
try {
if (boost::filesystem::is_regular_file(path) &&
((it.status().permissions() & 04000) == 04000 ||
(it.status().permissions() & 02000) == 02000)) {
// store path
r["path"] = boost::lexical_cast<std::string>(path);
// store user and group
if (stat(path.c_str(), &info) == 0) {
struct passwd *pw = getpwuid(info.st_uid);
struct group *gr = getgrgid(info.st_gid);
// get user name
r["unix_user"] = pw ? boost::lexical_cast<std::string>(pw->pw_name)
: boost::lexical_cast<std::string>(info.st_uid);
// get group
r["unix_group"] = gr ? boost::lexical_cast<std::string>(gr->gr_name)
: boost::lexical_cast<std::string>(info.st_gid);
// get permission
r["permissions"] = "";
r["permissions"] +=
(it.status().permissions() & 04000) == 04000 ? "S" : "";
r["permissions"] +=
(it.status().permissions() & 02000) == 02000 ? "G" : "";
results.push_back(r);
}
int perms = it.status().permissions();
if (boost::filesystem::is_regular_file(path) &&
((perms & 04000) == 04000 || (perms & 02000) == 02000)) {
genBin(path, perms, results);
}
} catch (...) {
// handle invalid files like /dev/fd/3

View File

@ -0,0 +1,7 @@
// Copyright 2004-present Facebook. All Rights Reserved.
/*
** This file is generated. Do not modify it manually!
*/
void __blacklisted_{{table_name}}() {}

View File

@ -0,0 +1,196 @@
// Copyright 2004-present Facebook. All Rights Reserved.
/*
** This file is generated. Do not modify it manually!
*/
#include <cstring>
#include <string>
#include <vector>
#include <boost/lexical_cast.hpp>
#include "osquery/database.h"
#include "osquery/tables/base.h"
#include "osquery/registry/registry.h"
namespace osquery { namespace tables {
{% if class_name == "" %}\
osquery::QueryData {{function}}();
{% else %}
class {{class_name}} {
public:
static osquery::QueryData {{function}}();
};
{% endif %}\
struct sqlite3_{{table_name}} {
int n;
{% for col in schema %}\
std::vector<{{col.type}}> {{col.name}};
{% endfor %}\
};
const std::string
sqlite3_{{table_name}}_create_table_statement =
"CREATE TABLE {{table_name}}("
{% for col in schema %}\
"{{col.name}} \
{% if col.type == "std::string" %}VARCHAR{% endif %}\
{% if col.type == "int" %}INTEGER{% endif %}\
{% if col.type == "long long int" %}BIGINT{% endif %}\
{% if not loop.last %}, {% endif %}"
{% endfor %}\
")";
int {{table_name_cc}}Create(
sqlite3 *db,
void *pAux,
int argc,
const char *const *argv,
sqlite3_vtab **ppVtab,
char **pzErr
) {
return xCreate<
x_vtab<sqlite3_{{table_name}}>,
sqlite3_{{table_name}}
>(
db, pAux, argc, argv, ppVtab, pzErr,
sqlite3_{{table_name}}_create_table_statement.c_str()
);
}
int {{table_name_cc}}Column(
sqlite3_vtab_cursor *cur,
sqlite3_context *ctx,
int col
) {
base_cursor *pCur = (base_cursor*)cur;
x_vtab<sqlite3_{{table_name}}> *pVtab =
(x_vtab<sqlite3_{{table_name}}>*)cur->pVtab;
if(pCur->row >= 0 && pCur->row < pVtab->pContent->n) {
switch (col) {
{% for col in schema %}\
// {{ col.name }}
case {{ loop.index0 }}:
{% if col.type == "std::string" %}\
sqlite3_result_text(
ctx,
(pVtab->pContent->{{col.name}}[pCur->row]).c_str(),
-1,
nullptr
);
{% endif %}\
{% if col.type == "int" %}\
sqlite3_result_int(
ctx,
(int)pVtab->pContent->{{col.name}}[pCur->row]
);
{% endif %}\
{% if col.type == "long long int" %}\
sqlite3_result_int64(
ctx,
(long long int)pVtab->pContent->{{col.name}}[pCur->row]
);
{% endif %}\
break;
{% endfor %}\
}
}
return SQLITE_OK;
}
int {{table_name_cc}}Filter(
sqlite3_vtab_cursor *pVtabCursor,
int idxNum,
const char *idxStr,
int argc,
sqlite3_value **argv
) {
base_cursor *pCur = (base_cursor *)pVtabCursor;
x_vtab<sqlite3_{{table_name}}> *pVtab =
(x_vtab<sqlite3_{{table_name}}>*)pVtabCursor->pVtab;
pCur->row = 0;
{% for col in schema %}\
pVtab->pContent->{{col.name}}.clear();
{% endfor %}\
{% if class_name != "" %}
for (auto& row : osquery::tables::{{class_name}}::{{function}}()) {
{% else %}
for (auto& row : osquery::tables::{{function}}()) {
{% endif %}
{% for col in schema %}\
{% if col.type == "std::string" %}\
pVtab->pContent->{{col.name}}.push_back(row["{{col.name}}"]);
{% endif %}\
{% if col.type == "int" %}\
try {
pVtab->pContent->{{col.name}}\
.push_back(boost::lexical_cast<int>(row["{{col.name}}"]));
} catch (const boost::bad_lexical_cast& e) {
LOG(WARNING) << "Error casting " << row["{{col.name}}"] << " to int";
pVtab->pContent->{{col.name}}.push_back(-1);
}
{% endif %}\
{% if col.type == "long long int" %}\
try {
pVtab->pContent->{{col.name}}\
.push_back(boost::lexical_cast<long long>(row["{{col.name}}"]));
} catch (const boost::bad_lexical_cast& e) {
LOG(WARNING) << "Error casting " << row["{{col.name}}"] << " to long long int";
pVtab->pContent->{{col.name}}.push_back(-1);
}
{% endif %}\
{% endfor %}\
}
pVtab->pContent->n = pVtab->pContent->{{schema[0].name}}.size();
return SQLITE_OK;
}
static sqlite3_module {{table_name_cc}}Module = {
0,
{{table_name_cc}}Create,
{{table_name_cc}}Create,
xBestIndex,
xDestroy<x_vtab<sqlite3_{{table_name}}>>,
xDestroy<x_vtab<sqlite3_{{table_name}}>>,
xOpen<base_cursor>,
xClose<base_cursor>,
{{table_name_cc}}Filter,
xNext<base_cursor>,
xEof<base_cursor, x_vtab<sqlite3_{{table_name}}>>,
{{table_name_cc}}Column,
xRowid<base_cursor>,
0,
0,
0,
0,
0,
0,
0,
};
class {{table_name_cc}}TablePlugin : public TablePlugin {
public:
{{table_name_cc}}TablePlugin() {}
int attachVtable(sqlite3 *db) {
return sqlite3_attach_vtable<sqlite3_{{table_name}}>(
db, "{{table_name}}", &{{table_name_cc}}Module);
}
virtual ~{{table_name_cc}}TablePlugin() {}
};
REGISTER_TABLE(
"{{table_name}}",
std::make_shared<{{table_name_cc}}TablePlugin>()
);
}}

View File

@ -18,218 +18,11 @@ DEVELOPING = False
# the log format for the logging module
LOG_FORMAT = "%(levelname)s [Line %(lineno)d]: %(message)s"
# BL_IMPL_TEMPLATE is the jinja template used to generate the virtual table
# implementation file when the table is blacklisted in ./osquery/tables/specs
BL_IMPL_TEMPLATE = """// Copyright 2004-present Facebook. All Rights Reserved.
# Read all implementation templates
TEMPLATES = {}
/*
** This file is generated. Do not modify it manually!
*/
void __blacklisted_{{table_name}}() {}
"""
# IMPL_TEMPLATE is the jinja template used to generate the virtual table
# implementation file
IMPL_TEMPLATE = """// Copyright 2004-present Facebook. All Rights Reserved.
/*
** This file is generated. Do not modify it manually!
*/
#include <cstring>
#include <string>
#include <vector>
#include <boost/lexical_cast.hpp>
#include "osquery/database.h"
#include "osquery/tables/base.h"
#include "osquery/registry/registry.h"
namespace osquery { namespace tables {
{% if class_name == "" %}
osquery::QueryData {{function}}();
{% else %}
class {{class_name}} {
public:
static osquery::QueryData {{function}}();
};
{% endif %}
struct sqlite3_{{table_name}} {
int n;
{% for col in schema %}\
std::vector<{{col.type}}> {{col.name}};
{% endfor %}\
};
const std::string
sqlite3_{{table_name}}_create_table_statement =
"CREATE TABLE {{table_name}}("
{% for col in schema %}\
"{{col.name}} \
{% if col.type == "std::string" %}VARCHAR{% endif %}\
{% if col.type == "int" %}INTEGER{% endif %}\
{% if col.type == "long long int" %}BIGINT{% endif %}\
{% if not loop.last %}, {% endif %}"
{% endfor %}\
")";
int {{table_name_cc}}Create(
sqlite3 *db,
void *pAux,
int argc,
const char *const *argv,
sqlite3_vtab **ppVtab,
char **pzErr
) {
return xCreate<
x_vtab<sqlite3_{{table_name}}>,
sqlite3_{{table_name}}
>(
db, pAux, argc, argv, ppVtab, pzErr,
sqlite3_{{table_name}}_create_table_statement.c_str()
);
}
int {{table_name_cc}}Column(
sqlite3_vtab_cursor *cur,
sqlite3_context *ctx,
int col
) {
base_cursor *pCur = (base_cursor*)cur;
x_vtab<sqlite3_{{table_name}}> *pVtab =
(x_vtab<sqlite3_{{table_name}}>*)cur->pVtab;
if(pCur->row >= 0 && pCur->row < pVtab->pContent->n) {
switch (col) {
{% for col in schema %}\
// {{ col.name }}
case {{ loop.index0 }}:
{% if col.type == "std::string" %}\
sqlite3_result_text(
ctx,
(pVtab->pContent->{{col.name}}[pCur->row]).c_str(),
-1,
nullptr
);
{% endif %}\
{% if col.type == "int" %}\
sqlite3_result_int(
ctx,
(int)pVtab->pContent->{{col.name}}[pCur->row]
);
{% endif %}\
{% if col.type == "long long int" %}\
sqlite3_result_int64(
ctx,
(long long int)pVtab->pContent->{{col.name}}[pCur->row]
);
{% endif %}\
break;
{% endfor %}\
}
}
return SQLITE_OK;
}
int {{table_name_cc}}Filter(
sqlite3_vtab_cursor *pVtabCursor,
int idxNum,
const char *idxStr,
int argc,
sqlite3_value **argv
) {
base_cursor *pCur = (base_cursor *)pVtabCursor;
x_vtab<sqlite3_{{table_name}}> *pVtab =
(x_vtab<sqlite3_{{table_name}}>*)pVtabCursor->pVtab;
pCur->row = 0;
{% for col in schema %}\
pVtab->pContent->{{col.name}}.clear();
{% endfor %}\
{% if class_name != "" %}
for (auto& row : osquery::tables::{{class_name}}::{{function}}()) {
{% else %}
for (auto& row : osquery::tables::{{function}}()) {
{% endif %}
{% for col in schema %}\
{% if col.type == "std::string" %}\
pVtab->pContent->{{col.name}}.push_back(row["{{col.name}}"]);
{% endif %}\
{% if col.type == "int" %}\
try {
pVtab->pContent->{{col.name}}\
.push_back(boost::lexical_cast<int>(row["{{col.name}}"]));
} catch (const boost::bad_lexical_cast& e) {
LOG(WARNING) << "Error casting " << row["{{col.name}}"] << " to int";
pVtab->pContent->{{col.name}}.push_back(-1);
}
{% endif %}\
{% if col.type == "long long int" %}\
try {
pVtab->pContent->{{col.name}}\
.push_back(boost::lexical_cast<long long>(row["{{col.name}}"]));
} catch (const boost::bad_lexical_cast& e) {
LOG(WARNING) << "Error casting " << row["{{col.name}}"] << " to long long int";
pVtab->pContent->{{col.name}}.push_back(-1);
}
{% endif %}\
{% endfor %}\
}
pVtab->pContent->n = pVtab->pContent->{{schema[0].name}}.size();
return SQLITE_OK;
}
static sqlite3_module {{table_name_cc}}Module = {
0,
{{table_name_cc}}Create,
{{table_name_cc}}Create,
xBestIndex,
xDestroy<x_vtab<sqlite3_{{table_name}}>>,
xDestroy<x_vtab<sqlite3_{{table_name}}>>,
xOpen<base_cursor>,
xClose<base_cursor>,
{{table_name_cc}}Filter,
xNext<base_cursor>,
xEof<base_cursor, x_vtab<sqlite3_{{table_name}}>>,
{{table_name_cc}}Column,
xRowid<base_cursor>,
0,
0,
0,
0,
0,
0,
0,
};
class {{table_name_cc}}TablePlugin : public TablePlugin {
public:
{{table_name_cc}}TablePlugin() {}
int attachVtable(sqlite3 *db) {
return sqlite3_attach_vtable<sqlite3_{{table_name}}>(
db, "{{table_name}}", &{{table_name_cc}}Module);
}
virtual ~{{table_name_cc}}TablePlugin() {}
};
REGISTER_TABLE(
"{{table_name}}",
std::make_shared<{{table_name_cc}}TablePlugin>()
);
}}
"""
# Temporary reserved column names
RESERVED = ["group"]
def usage():
""" print program usage """
@ -243,6 +36,35 @@ def to_camel_case(snake_case):
def lightred(msg):
return "\033[1;31m %s \033[0m" % str(msg)
def is_blacklisted(path, table_name):
"""Allow blacklisting by tablename."""
specs_path = os.path.dirname(os.path.dirname(path))
blacklist_path = os.path.join(specs_path, "blacklist")
if not os.path.exists(blacklist_path):
return False
try:
with open(blacklist_path, "r") as fh:
blacklist = [line.strip() for line in fh.read().split("\n")
if len(line.strip()) > 0 and line.strip()[0] != "#"]
if table_name in blacklist:
return True
except:
# Blacklist is not readable.
pass
return False
def setup_templates(path):
tables_path = os.path.dirname(os.path.dirname(os.path.dirname(path)))
templates_path = os.path.join(tables_path, "templates")
if not os.path.exists(templates_path):
print ("Cannot read templates path: %s" % (templates_path))
exit(1)
for template in os.listdir(os.path.join(tables_path, "templates")):
template_name = template.split(".", 1)[0]
with open(os.path.join(templates_path, template), "rb") as fh:
TEMPLATES[template_name] = fh.read().replace("\\\n", "")
pass
class Singleton(object):
"""
Make sure that anything that subclasses Singleton can only be instantiated
@ -278,10 +100,10 @@ class TableState(Singleton):
def foreign_keys(self):
return [i for i in self.schema if isinstance(i, ForeignKey)]
def generate(self, path, template=IMPL_TEMPLATE):
def generate(self, path, template="default"):
"""Generate the virtual table files"""
logging.debug("TableState.generate")
self.impl_content = jinja2.Template(template).render(
self.impl_content = jinja2.Template(TEMPLATES[template]).render(
table_name=self.table_name,
table_name_cc=to_camel_case(self.table_name),
schema=self.columns(),
@ -291,6 +113,14 @@ class TableState(Singleton):
class_name=self.class_name
)
# Check for reserved column names
for column in self.columns():
if column.name in RESERVED:
print (lightred(("Cannot use column name: %s in table: %s "
"(the column name is reserved)" % (
column.name, self.table_name))))
exit(1)
path_bits = path.split("/")
for i in range(1, len(path_bits)):
dir_path = ""
@ -305,7 +135,7 @@ class TableState(Singleton):
def blacklist(self, path):
print (lightred("Blacklisting generated %s" % path))
logging.debug("blacklisting %s" % path)
self.generate(path, template=BL_IMPL_TEMPLATE)
self.generate(path, template="blacklist")
table = TableState()
@ -374,23 +204,6 @@ def implementation(impl_string):
def description(text):
table.description = text
def is_blacklisted(path, table_name):
"""Allow blacklisting by tablename."""
specs_path = os.path.dirname(os.path.dirname(path))
blacklist_path = os.path.join(specs_path, "blacklist")
if not os.path.exists(blacklist_path):
return False
try:
with open(blacklist_path, "r") as fh:
blacklist = [line.strip() for line in fh.read().split("\n")
if len(line.strip()) > 0 and line.strip()[0] != "#"]
if table_name in blacklist:
return True
except:
# Blacklist is not readable.
pass
return False
def main(argc, argv):
if DEVELOPING:
logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG)
@ -407,6 +220,7 @@ def main(argc, argv):
# Adding a 3rd parameter will enable the blacklist
disable_blacklist = argc > 3
setup_templates(filename)
with open(filename, "rU") as file_handle:
tree = ast.parse(file_handle.read())
exec(compile(tree, "<string>", "exec"))

View File

@ -4,13 +4,26 @@ function platform() {
local __resultvar=$1
if [[ -f "/etc/redhat-release" ]]; then
eval $__resultvar="centos"
elif [[ -f "/etc/debian_version" ]]; then
elif [[ -f "/etc/lsb-release" ]]; then
eval $__resultvar="ubuntu"
elif [[ -f "/etc/pf.conf" ]]; then
eval $__resultvar="darwin"
fi
}
function distro() {
local __resultvar=$2
if [[ $1 = "centos" ]]; then
eval $__resultvar="centos"`cat /etc/redhat-release | awk '{print $3}'`
elif [[ $1 = "ubuntu" ]]; then
eval $__resultvar=`cat /etc/*-release | grep DISTRIB_CODENAME | awk -F '=' '{print $2}'`
elif [[ $1 = "darwin" ]]; then
eval $__resultvar=`sw_vers -productVersion | awk -F '.' '{print $1 "." $2}'`
else
eval $__resultvar="unknown_version"
fi
}
function threads() {
local __resultvar=$1
platform OS
@ -53,3 +66,4 @@ function contains_element() {
for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done
return 1
}

View File

@ -241,13 +241,15 @@ function check() {
HASH=`sha1sum $0 | awk '{print $1}'`
fi
if [[ ! "$1" = "check" ]]; then
if [[ "$1" = "build" ]]; then
echo $HASH > "$2/.provision"
return
elif [[ ! "$1" = "check" ]]; then
return
fi
if [[ "$#" < 2 ]]; then
echo "Usage: $0 check BUILD_PATH"
echo "Usage: $0 (check|build) BUILD_PATH"
exit 1
fi
@ -261,17 +263,22 @@ function check() {
function main() {
platform OS
distro $OS DISTRO
if [[ $1 = "get_platform" ]]; then
echo "$OS;$DISTRO"
return 0
fi
mkdir -p "$WORKING_DIR"
cd "$WORKING_DIR"
if [[ $OS = "centos" ]]; then
log "detected centos"
log "detected centos ($DISTRO)"
elif [[ $OS = "ubuntu" ]]; then
log "detected ubuntu"
DISTRO=`cat /etc/*-release | grep DISTRIB_CODENAME | awk '{split($0,bits,"="); print bits[2]}'`
log "detected ubuntu ($DISTRO)"
elif [[ $OS = "darwin" ]]; then
log "detected mac os x"
log "detected mac os x ($DISTRO)"
else
fatal "could not detect the current operating system. exiting."
fi
@ -445,4 +452,4 @@ function main() {
}
check $1 $2
main
main $1