#!/usr/bin/perl -w
# Script to lock/unlock system files by making them immutable.
# Unlocking must be done as root from single user mode.
# This is derived from code by George Shaffer:
# ---
# Copyright 2000, 2001 GeodSoft, LLC - geodsoft.com /
# George Shaffer (gshaffer@erols.com)
# Anyone may use or modify this code for any purpose PROVIDED
# that as long as it is recognizably derived from this code,
# that this copyright notice, remains intact and unchanged.
# No warrantees of any kind are expressed or implied.
# ---
# Written 11 June 2004 by Jim Lippard (version 1.0).
# Modified 9 November 2006 by Jim Lippard to add do-not-recurse option (version 1.1).
#    File paths beginning with a + will not have contents of subdirectories locked or
#    unlocked.
# Modified 27 December 2011 by Jim Lippard to determine system or user
#    immutability from syslock.conf and add Linux support.
# ToDo: Don't try to remount / unless necessary.

### Required packages.

use strict;

### Global constants.

my $CONFIG_FILE = '/etc/syslock.conf';

my $VERSION = '1.2 of 27 December 2011';

my $LOCK = 0;
my $UNLOCK = 1;

my $BSD_SYS_IMMUTABLE_FLAG_ON = 'schg';
my $BSD_USER_IMMUTABLE_FLAG_ON = 'uchg';
my $LINUX_IMMUTABLE_FLAG_ON = '+i';
my $LINUX_IMMUTABLE_FLAG_OFF = '-i';

my $MOUNT_COMMAND = 'mount -uw /';
my $BSD_LOCK_COMMAND = '/usr/bin/chflags';
my $LINUX_LOCK_COMMAND = '/usr/bin/chattr';
my $RECURSE_FLAG = '-R';
my $BSD_SECURELEVEL_COMMAND = '/sbin/sysctl kern.securelevel';

### Global variables.

my ($mode, $level, @files, $file, %dont_recurse);

my ($bsd, $linux, $lock_command, $lock_flag, $unlock_flag);

### Main program.

if ($0 =~ /syslock$/) {
    $mode = $LOCK;
}
elsif ($0 =~ /sysunlock$/) {
    $mode = $UNLOCK;
}
else {
    die "syslock/sysunlock: I've been invoked under someone else's name: $0\n";
}

$bsd = $linux = 0;

# BSD or Linux?
if (-e $BSD_LOCK_COMMAND) {
    $bsd = 1;
    # Default is system immutability, but this is changeable in config file.
    $lock_command = $BSD_LOCK_COMMAND;
    $lock_flag = $BSD_SYS_IMMUTABLE_FLAG_ON;
    $unlock_flag = "no$lock_flag";
}
elsif (-e $LINUX_LOCK_COMMAND) {
    $linux = 1;
    $lock_command = $LINUX_LOCK_COMMAND;
    $lock_flag = $LINUX_IMMUTABLE_FLAG_ON;
    $unlock_flag = $LINUX_IMMUTABLE_FLAG_OFF;
}
else {
    die "Immutability flags do not appear to be supported by your system.\n";
}

open (CONFIG, $CONFIG_FILE) || die "Cannot open config file. $CONFIG_FILE\n";
while (<CONFIG>) {
    chop;
    if (/^\s*$|^\s*#/) { # Comment or blank space.
    }
    elsif (/^immutable-flag:\s+(\w+)$/) {
	if (($1 eq $BSD_SYS_IMMUTABLE_FLAG_ON) ||
		     ($1 eq $BSD_USER_IMMUTABLE_FLAG_ON)) {
	    if ($linux) {
		die "Config file specifies BSD lock flag type but this appears to be a Linux system.\n";
	    }
	    $lock_flag = $1;
	    $unlock_flag = "no$1";
	}
	elsif ($1 eq $LINUX_IMMUTABLE_FLAG_ON) {
	    if ($bsd) {
		die "Config file specifies Linux lock flag type but this appears to be a Linux system.\n";
	    }
	}
	else {
	    die "Config file specifies unknown lock flag type \"$1\".\n";
	}
    }
    elsif (/^\+(.*)/) {
	$dont_recurse{$1} = 1;
	push (@files, $1);
    }
    else {
	push (@files, $_);
    }
}
close (CONFIG);

# If lock, no securelevel requirement.
# If unlock, BSD, and using system immutability, securelevel must be < 2.
if ($bsd && $lock_flag eq $BSD_SYS_IMMUTABLE_FLAG_ON) {
    $level = `$BSD_SECURELEVEL_COMMAND`;
    $level =~ s/kern\.securelevel\s*=\s*(\d+)/$1/;
    if ($mode == $UNLOCK && $level > 1) {
	die "System securelevel is $level; sysunlock requires securelevel < 2.\n";
    }
}

# If / happens to be mounted as readonly, make it writeable.
system "$MOUNT_COMMAND";

foreach $file (@files) {
    if ($mode == $LOCK) {
	if (-d $file) {
	    if ($dont_recurse{$file}) {
		system "$lock_command $lock_flag $file/*";
		system "$lock_command $lock_flag $file";
	    }
	    else {
		system "$lock_command $RECURSE_FLAG $lock_flag $file";
	    }
	}
	else {
	    system "$lock_command $lock_flag $file";
	}
    }
    else {
	if (-d $file) {
	    if ($dont_recurse{$file}) {
		system "$lock_command $unlock_flag $file";
		system "$lock_command $unlock_flag $file/*";
	    }
	    else {
		system "$lock_command $RECURSE_FLAG $unlock_flag $file";
	    }
	}
	else {
	    system "$lock_command $unlock_flag $file";
	}
    }
}
