A TERMINAL MONITOR MADE EASY
Raymond J. Ikola
The simplest way to defeat security measures for data in a confidential or restricted account is simply to wait for an authorized user to leave the terminal unattended without logging off while taking a lunch break -- or worse yet, going home for the evening. Password protection, update locks, and retrieval locks, will not protect the data when the authorized user leaves the terminal unattended, but in an accessible state.
Another problem created by unattended inactive terminals is the potential for tying up a system when the maximum number of licensed users are logged on, many of whom are not actually using their terminal, thereby preventing others who need to use the system from logging on. [Note that with AP Native 5.2, 33 serial ports are provided, but the number of concurrently signed on users is limited by the license for your particular system.] Killing the inactive ports automatically surely beats calling around to see who is able to logoff.
With AP's phantom processor, the problem may easily be minimized by running a background process to check for terminal inactivity. Whenever the terminal is inactive for a predetermined time, the background process will log it off. The listing presented here, called "term.monitor," is a "barebones" monitor to accomplish this basic task. Bells and whistles can be added to your heart's content. I have been using a variant of this routine for some time now on AP, and it is currently running on AP Native 5.2. Some of the code will not be supported by other Pick implementations, so review it carefully for compatibility and modify accordingly.
The terminal monitor is started as a phantom process by including a macro in the master dictionary whose item id is the account name. Any log to the account will automatically run the macro. The macro can either run a basic program which presents a customized logon display, or a control routine, or perhaps it will call a menu using the AP menu processor. If a basic program is run by the macro, the monitor is turned on by executing " z term.monitor ". The simplest device, however, is to start the monitor directly with the macro. See the comments at lines 39-41 for a sample macro.
The first user to log to the account while it is not in use will start the monitor as a phantom process. The first line of code will set a basic lock for process 0. As additional users log on, the phantom process will be initiated, but the monitor will be stopped immediately when it discovers that basic lock 0 is already set. In this way, only one monitor is running in the background for each account. Of course, if you are using basic lock 0 to prevent reentry for other parts of your application, choose another available basic lock to identify the monitor process.
A file called "md.ctrl" is assumed to exist, and, in this demonstration listing, is created if not already in existence. If your application has some other file which contains parameters for your application, you may use whatever file seems appropriate. Only one item is in the assumed "md.ctrl" file, called "monitor.params." The monitor.params item is simply a listing of the time period each terminal on the system is permitted to remain inactive without being logged off. These permitted "max.quiet" time periods are expressed in seconds. See the comments at lines 282-288 for the structure of the monitor.params item.
The code listing itself is heavily documented and should be largely self-explanatory. The brief description which follows will hopefully clear up any lingering questions, warn of traps for the unwary, as well as to suggest areas for your own enhancement.
Setting the Lock.
The basic lock is a system-wide lock. However, this program is presented in a form designed to be installed on a single account. If you install this program on several accounts, the monitor will not be activated for a new logon to an account if another account has already started the monitor. Thus, if you plan on using the monitor on more than one account, the program is best installed in the dm account. Each account for which the monitor is to be activated could then start the monitor with the command " z run dm,bp, term.monitor ." Alternatively, catalog dm,bp, term.monitor in each of the desired accounts and use a macro which simply commands " z term.monitor ." If you wish the monitor to oversee the entire system, each user in the dm,users, file should have the command " z term.monitor " in the logon macro. Note: If the monitor is to oversee more than one account, the access statement defined at lines 109-110, and executed, e.g., at line 111, must be modified appropriately.
Subroutines set.port.list and set.port.arrays .
The subroutine set.port.list is called whenever there is a need to obtain
a current list of the ports and user.ids which need to be monitored. Because
users may be logging on and logging off while the monitor is running,
each time the activity is checked, a current list needs to be established.
Set.port.list is always preceded by an executed access statement which
returns a list of pairs of port numbers and user.ids, each pair separated
by an attribute mark. This array is split into two arrays called port.lst
and user.lst , with the elements of the two arrays matched.
Thus, port.lst
The subroutine set.port.arrays is used to initialize the arrays port.charges and port.time for a port which logs on for the first time. Each time a port drops out of the user mix being monitored, the corresponding element of port.charges and port.time is set to zero. When that port later appears as an active port, it is detected as newly logged on by the zero in the array port.time , and the arrays are then initialized.
Subroutine get.monitor.params.
The subroutine get.monitor.params simply reads the list of max.quiet periods for each port from item monitor.params in the md.ctrl file. Because it is assumed that the application will have a utility routine which can update and change these parameters, the parameters are freshly read before each check of the port usage to assure that any changed quiet periods are picked up. The data is massaged somewhat, however, to avoid the ridculous. If your utility routine to update these parameters is set up to apply appropriate limits, this subroutine need only read the data.
The limits on this data seem appropriate from experience, but you may wish to change the limitations. First, the period at which a terminal will be logged off for inactivity is set to a minimum of six minutes ( max.quiet = 360). Having a terminal go down with a shorter permitted period of inactivity could be quite annoying. If the parameter has not been set for the terminal, 30 minutes is set as a default ( max.quiet = 1800). The variable chk.freq defines the number of seconds between each review of the port usage. This parameter is set to review the ports six times during the smallest permitted period of inactivity. Thus, for example, if port 2 allows 30 minutes of inactivity, but all other ports allow 60 minutes of inactivity, the ports will be reviewed for usage every five minutes. Finally, if the permitted periods of inactivity are all rather long, the chk.freq parameter will be set to review the ports at least once every five minutes. Feel free to change these limits as may be appropriate.
The Port Usage Review.
The main routine is a loop found at lines 119 through 211. The loop continues to run while the variable users.active is greater than zero, i.e., while anyone (excluding the phantom) is still logged on.
The loop begins by sleeping for the time chk.freq (set to zero on the first pass). A check is then made to determine whether the midnight time has been passed. Each user is given a fresh start at midnight by setting the port.charges and port.time to zero. No one will be logged off on this pass, because the port.charges will never be equal to the current charges for the port. (See line 166). With that test failing, the arrays will be reinitialized for the next pass. (See lines 181-182). The code could be enhanced here to provide a seamless crossing of the midnight line, but it's doubtful that the extra effort will have much value.
The current monitor parameters are then read from the file, and a current list of users and ports is obtained (lines 133-139). The current list of users is compared with the list saved from the last pass (line 140), and if the user mix has changed, the arrays for port.charges and port.time for the ports which have dropped out are set to zero to signal a need for reinitialization if they later come back on line. The current arrays are updated with a change of the user mix. (See lines 146-156).
With the current user lists in hand, each of the active ports is reviewed. (See lines 161-184). The current CPU units are obtained for each active port (line 165), and the current CPU units are compared with the CPU units recorded on the last pass. (See line 166). If the CPU units are equal on this comparison, the terminal is deemed to have been inactive since the last pass, and a time check is made. (See line 167). If the current time exceeds the last recorded time for the port plus the permitted inactivity period, the port is logged off. If not, the arrays for the port.charges and port.time are updated to the current values. (See lines 181-182).
Note that the array port.time is updated to the current time only when there has been a change in the CPU units for that port since the last pass. This means that if a port is used immediately after a pass, the next pass will update the port.time and port.charges , and inactivity of the port will not be detected until the full permitted inactivity period has elapsed since the update. This scheme means that a port may be inactive for nearly the chk.freq period plus the permitted inactivity period before being logged off. This should be kept in mind when changing the limits of the paramter chk.freq . With chk.freq set to do a review at least every five minutes, no port will remain logged on which has been inactive for more than five minutes past the so-called permitted inactivity period.
When the inactivity check has indicated that a port should be logged off, the action is accomplished by executing the tcl command directed to the quiet port. (See lines 171-175). The command monitor.message may either be a cataloged basic program, or a macro which displays an appropriate message and then logs the terminal off. See the comments at lines 50-56 for an appropriate macro.
After all ports are reviewd, the program must determine whether to continue with another pass through the loop or to quit. The code at lines 204-211 needs some explanation. During the reviewing process in lines 161-184, some ports may have been logged off for inactivity, but other ports may have logged on which are not yet reflected in the port.lst. Thus, to determine whether to kill the monitor, another check must be made to determine whether any ports are still active. (See lines 205-207). The problem is that while the access statement is executing, and the new array is being established, another logon could occur after the list of current users is retrieved, but before the basic lock is released. If this happened, the newly logged on user would not start the monitor, because the basic lock is still set, but the monitor program would nevertheless stop because the new user was not on when the access statement was processed. To avoid the possibility of a new user "sneaking on" the system without triggering the monitor, new users are temporarily blocked from logging on to the account. This is accomplished by replacing the startup macro with a new macro which displays a message and immediately logs a new user off. This macro is the variable inhibit.log found at lines 90-91. If the users are detected as being on the account, the loop will continue for another pass, but before doing so, the original macro is restored. (See line 209). If all users are detected as being off, the basic lock is released first, and then the original macro is restored. In this way, a successful logon will always start the monitor.
Additional Security
By now you it will have occurred to you that a knowlegeable user may always defeat the monitor by simply deleting the macro which starts the monitor, or by simply logging off the phantom port which is running the monitor. Although no scheme is perfect, some practical ways to prevent all but the most ardent security-breakers, can easily be implemented. For starters, design the application to prevent access to TCL except for designated users. Second, control the application with a routine which checks at key points whether the monitor is running. If it's off, turn it on. For example, put the following code at key locations in your control routine.
lock 0 then unlock 0 execute 'z term.monitor' capturing xx end
One who is intent on defeating the monitor will find a way. But remember, the monitor is principally to protect against inadvertent access being gained through an unattended terminal which has otherwise passed the security check points and to avoid tying up the system with the maximum number of licensed users being logged on with unattended terminals.
term.monitor
* BP TERM.MONITOR
*---------------------------------------------------------------------
* TERM.MONITOR *
* COPYRIGHT (C) 1991 BY RAYMOND J. IKOLA *
* ALL RIGHTS RESERVED *
*---------------------------------------------------------------------
* Author: Raymond J. Ikola
* Date of Last Change: 08/25/92
* First Written: 03/31/91
* Description: Monitors terminal activity (Database Digest Version)
* Library: Control
* Number: 00
* Version: 1.0
*---------------------------------------------------------------------
* Includes: None *
* Subroutine Calls: Execute TCL command to MONITOR.MESSAGE *
* Called From: Main Control Program (as phantom process) *
* or from macro *
* Arguments Passed: None *
*---------------------------------------------------------------------
* Notes: *
* *
* This program is run as a phantom process. It monitors the *
* usage of ports which are logged on to the account and logs off the *
* inactive ports. *
* *
* When the monitor is activated, it sets a basic lock for *
* process 0. Reentry is prevented by checking for the lock. Thus, *
* the monitor is started by the first port to log to the account *
* while the monitor is inactive. *
* *
* The monitor may be started from a control program which is *
* activated from a macro with item.id = md.name, or the macro may *
* itself start the monitor. Use a basic program if you wish to *
* present a fancier display when logging to the account and execute *
* "z term.monitor" from the basic program. If a "fancy display" is *
* of no concern, the following macro in the MD will do the job. *
* *
* MD item: md.name *
* 01 n *
* 02 z term.monitor *
* *
* The cataloged command MONITOR.MESSAGE is either a macro or *
* a cataloged basic program which displays a message on the terminal *
* of the user being logged off and then logs off. Again, use a *
* basic program for fancier displays. A macro will do the job just *
* as nicely, but with less flexibility in the display. For example, *
* the following macro in the MD will do the job. *
* *
* MD item: monitor.message *
* 01 n *
* 02 comment (-1)(-13)( Sorry! This terminal is being logged *
* off because it is inactive. )(-14) *
* 03 comment (-13)( An unattended terminal creates a security *
* risk for your data. ) *
* 04 off *
* *
* Variables: port.lst -- dynamic array of PIBS currently *
* logged on. *
* user.lst -- dynamic array of users currently *
* logged on. *
* port.charges -- dimensioned array of CPU charges *
* for each port. *
* port.time -- dimensioned array of time of day *
* last checked for each port. *
* who.lst -- dynamic array of ports and users *
* in form: port1 user1^port2 user2^ *
* . . .^portn usern *
* max.quiet -- dimensioned array of permitted *
* quiet periods for each port. *
*---------------------------------------------------------------------
*
!------------------+
! Prohibit reentry |
!------------------+
lock 0 else stop ;* Monitor already running
!-------------------------------------------+
! Initialize variables and dimension arrays |
!-------------------------------------------+
equ am to char(254)
equ get.charge to 'u3b' ;* User exit for CPU charges
no.sys.pibs = system(18) ;* Number of PIBS on system
monitor.pib = system(22) ;* PIB on which monitor running
dim port.Charges(no.sys.pibs)
dim port.time(no.sys.pibs)
dim max.quiet(no.sys.pibs)
who.lst = ''; last.who.lst = ''; ctrlm = ''
mat port.charges = 0; mat port.time = 0; mat max.quiet = 0
start.date = date()
inhibit.log = 'n':am:'comment (Logon temporarily inhibited)'
inhibit.log = inhibit.log:am:'comment (Try Again)':am:'off'
allow.log = 'n':am:'z term.monitor'
!----------------+
! Open the files |
!----------------+
open 'md.ctrl' to f.ctrl else
execute 'create-file md.ctrl 1 3' capturing xx
open 'md.ctrl' to f.ctrl else stop
end
open 'md' to f.md else stop
!-----------------+
! Get the md name |
!-----------------+
execute 'who' capturing who
md = field(who,' ',3) ;* Current md
!-------------------+
! Initialize arrays |
!-------------------+
inq = 'sort dm,pibs, with user and with account "':md:'" user'
inq = inq:' col-hdr-supp ni-supp'
execute inq capturing who.lst
who.lst = trim(who.lst)
gosub set.port.list: ;* Get lists of ports and users
gosub set.port.arrays: ;* Initialize the port arrays
chk.freq = 0 ;* Set first pass for immediate run
!-------------------------------------+
! Check status every chk.freq seconds |
!-------------------------------------+
loop
sleep chk.freq
!-------------------------+
! Fresh start at midnight |
!-------------------------+
if date() > start.date then
start.date = date()
mat port.time = 0
mat port.charges = 0
end
!----------------------------------------------------------------+
! Check monitor parameters on each pass just in case the system |
! user has changed the parameters since the last pass. |
!----------------------------------------------------------------+
gosub get.monitor.params:
!----------------------------------------------------------------+
! Check if port.lst needs to be redefined - user mix changed |
! since last pass. |
!----------------------------------------------------------------+
execute inq capturing who.lst
who.lst = trim(who.lst)
if who.lst # last.who.lst then
!-------------------------------------------------------------+
! Reset the time and charges for any user who has dropped out |
! Operates as a flag to reinitialize the time and charges on |
! the port if port logs on again later. |
!-------------------------------------------------------------+
for i = 1 to no.in.last.who.lst
tst.port = field(last.who.lst,am,i)
tst.num = field(tst.port,' ',1)
array.index = tst.num+1
locate tst.port in who.lst setting loc else ;* User is off
port.charges(array.index) = 0 ;* Set to zero to
port.time(array.index) = 0 ;* flag reinitialize
end
next i
gosub set.port.list:
gosub set.port.arrays:
end
!--------------------------------+
! Check usage of logged on ports |
!--------------------------------+
for i = 1 to users.active
cur.port = port.lst
array.index = cur.port+1
conv.param = 'c':cur.port
cur.charges = oconv(conv.param,get.charge)
if port.charges(array.index) = cur.charges then
if time() > port.time(array.index) + max.quiet(array.index) then
!------------------+
! Logoff this port |
!------------------+
quiet.user = user.lst
logoff = 'tcl ':cur.port:' ':quiet.user:' monitor.message'
execute logoff
port.charges(array.index) = 0
port.time(array.index) = 0
end
end else
!-----------------------------+
! Update charges and new time |
!-----------------------------+
port.charges(array.index) = cur.charges
port.time(array.index) = time()
end
next i
!---------------------------------------------------+
! The charges on all ports have been polled |
! Check whether anyone is still on or has logged on |
! during the review process. |
!---------------------------------------------------+
!-----------------------------------------------------------------+
! It takes time to ascertain that all ports are shut down. At |
! the end of this loop, a check is made to see if anyone has |
! logged on while the charges for all ports were being reviewed. |
! It can happen that someone logs on during this checking process |
! and since the basic lock 0 is still set, the monitor will not |
! be kicked on, even though the last check shows all users to be |
! logged off. To be sure nobody "sneaks on" without triggering |
! the monitor, the macro with the account name is changed for a |
! moment while this check is being made. When the check is |
! complete, the normal account macro is restored, allowing normal |
! logon. In practice, this temporary block of new logons should |
! rarely be observed. |
!-----------------------------------------------------------------+
write inhibit.log on f.md, md; *temporary inhibit of new logon
execute inq capturing who.lst
who.lst = trim(who.lst)
gosub set.port.list:
while users.active do
write allow.log on f.md, md
gosub set.port.arrays:
repeat
!---------------------------------------------------------+
! Everyone is off. Unlock the process and restore macro. |
!---------------------------------------------------------+
unlock 0
write allow.log on f.md, md
stop
*
*******************************************************************
*
*----------------------------------------------+
set.port.list: *Set up List of Ports and Users |
*----------------------------------------------+
*
port.lst = ''; user.lst = ''
users.active = dcount(who.lst,am)
for i = 1 to users.active
this.who = who.lst
port.no = field(this.who,' ',1)
sys.user = field(this.who,' ',2)
if port.no # monitor.pib then
port.lst<-1> = port.no
user.lst<-1> = sys.user
end
next i
last.who.lst = who.lst
no.in.last.who.lst = dcount(last.who.lst,am)
users.active = dcount(port.lst,am)
return
*
*******************************************************************
*
*---------------------------------------------------------+
set.port.arrays: *Initialize New Ports and Update Charges |
*---------------------------------------------------------+
*
!-------------------------------------------------------+
! Note: port.time is updated here only if the time is 0 |
! The zero acts as a flag to indicate that port is new |
!-------------------------------------------------------+
for i = 1 to users.active
cur.port = port.lst
array.index = cur.port+1
if port.time(array.index) = 0 then
conv.param = 'c':cur.port
cur.charges = oconv(conv.param,get.charge)
port.charges(array.index) = cur.charges
port.time(array.index) = time()
end
next i
return
*
***********************************************************************
*
*--------------------------------------------+
get.monitor.params: *Gets monitor parameters |
*--------------------------------------------+
*
!-------------------------------------------------------+
! Monitor parameters maintained in md.ctrl file in item |
! "monitor.params". If monitor parameters not set, |
! a default period of 30 minutes quiet time is set and |
! written to the file. If the monitor parameters are |
! set, but they are ridiculously low, (less than six |
! minutes, increase the quiet time to six minutes. The |
! checking frequency is 6 checks in every max quiet |
! period, with a minimum frequency of once every five |
! minutes. |
! |
! The maximum permitted quiet times may be set for each |
! port separately. The "monitor.params" item is: |
! Item: monitor.params |
! 1) max.quiet for port 0 (in seconds) |
! 2) max.quiet for port 1 (in seconds) |
! . |
! . |
! . |
! n) max.quiet for port n-1 (in seconds) |
!-------------------------------------------------------+
chk.freq = 300
matread max.quiet from f.ctrl, 'monitor.params' then
for i = 1 to no.sys.pibs
begin case
case max.quiet(i) = '' ; max.quiet(i) = 1800
case max.quiet(i) < 360; max.quiet(i) = 360
end case
tst.freq = int(max.quiet(i)/6)
if tst.freq < chk.freq then chk.freq = tst.freq
next i
end else
mat max.quiet = 1800
matwrite max.quiet on f.ctrl, 'monitor.params'
chk.freq = 300
end
if chk.freq > 300 then chk.freq = 300
return
*****
end
*****
