A few lines of code and you have your own object cloner

Class DC.ObjCloner [ Abstract ]
{
/// obj: Cache/IRIS or a Dynamic(JSON) Object
/// 
/// For simplicity, JSON-Types null, true, false, etc. are ignored
ClassMethod Clone(obj)
{
    if $isobject(obj) {
        if obj.%IsA("%DynamicAbstractObject") {
            if obj.%IsA("%DynamicObject") { set new={},arr=0 } else { set new=[], arr=1 }
            set iter=obj.%GetIterator()
            while iter.%GetNext(.key,.val) {
                set:$isobject(val) val=..Clone(val)
                do $case(arr, 1:new.%Push(val), :new.%Set(key,val))
            }
            quit new
            
        } else { quit obj.%ConstructClone(1) }
    } else { quit "" }
}
}
Class DC.Import Extends (%Persistent, %JSON.Adaptor)
{

Property thingone As %String;
Property thingtwo As %String;
ClassMethod Test()
{
	// create a test stream
	set myStream=##class(%Stream.TmpCharacter).%New()
	do myStream.Write("[{""thingone"":""Red"",""thingtwo"":""Green""},{""thingone"":""Blue"",""thingtwo"":""Yellow""}]")
	
	// convert input (JSON) stream into a JSON object
	set data={}.%FromJSON(myStream)
	
	// loop over the JSON-Array and import each array element into your database
	for i=0:1:data.%Size()-1 {
		set obj=..%New()
		set sts=obj.%JSONImport(data.%Get(i))
		if sts set sts=obj.%Save()
		if sts continue
		write "Error: i=",i,", reason=",$system.Status.GetOneErrorText(sts),!
	}
}

Of course, instead of "myStream" you should use your REST-input stream.

do ##class(DC.Import).Test()

zwrite ^DC.ImportD
^DC.ImportD=2
^DC.ImportD(1)=$lb("","Red","Green")
^DC.ImportD(2)=$lb("","Blue","Yellow")

Shouldn't be a problem, $ZTZ isn't the only thing you can change.

Whatever value or setting you change, $namespace, $zeof, $horolog, etc. you have to consider a simple rule, if you change things (which can be used by subsequent routines, methods, etc.) for your own use, then after using them, you have to restore them to their original value. That's all.

YouRESTorWHATEVERmethod()
{
  set oldZTZ=$ztz
  set $ztz=<your preferred value>
  ...
  set $ztz=oldZTZ
  return
}

You can change the Timezone system variable

//  for example EST (Eastern Standard Time, 5 hours behind UTC)
    set $ztz = 300
//  you can even consider daylight saving
//  for example CET (Central European Time, 1 hour ahead of UTC)
#define DSTOFFSET	-60
#; -60=CET, 300=EST
#define LOCALZONE   -60
    set $ztz = $$$LOCALZONE+$s($system.Util.IsDST():$$$DSTOFFSET,1:0)
    

Setting $ZTZ affects the current job only (the job, which sets $ztz), so there is no danger for other processes.

If you want, for whatever reason, to compute the CRC of a Stream then write an short method like this (or use this):

/// Input : str - a sting or a stream
///         typ - CRC-Type (0..7, See ISC documentation)
///         
/// Output: crc-value
/// 
ClassMethod CRC(str, typ = 7)
{
    i $isobject(str),str.%IsA("%Stream.Object") {
        d str.Rewind() s crc=0
        while 'str.AtEnd { s crc=$zcrc(str.Read(32000),typ,crc) }
        ret crc
        
    } else { ret $zcrc(str,typ) }
}

Where is the problem?

I already have a database (named APPLIB) where such classes, routines and (some) data are stored and mapped to %ALL. By the way, I use %APP since it was introduced.
Nevertheless, mapping the above mentioned class (Py.Utility) to APPLIB has the same effect: it works, if I'm in %SYS but does not work for in other (for example USER) namespace.
I thought, it's simpler to explain the problem if I use a percent-class (%Zpy.Utility).
 

USER>

USER>w ##class(Py.Utility).Info()

 set val = ..internal(arg)
 ^
<OBJECT DISPATCH>zInfo+1^Py.Utility.1 *python object not found
USER 2e1>q

USER>

USER>w ##class(%SYS.Namespace).GetPackageDest(,"Py")
^/opt/isc/icindy/db/applib/
USER>

USER>zn "%SYS"

%SYS>w ##class(Py.Utility).Info()
def
%SYS>

the above approach ist the right way. And I do not see any problem there:

First, in the very first line (of the question) it's stated: "I need to develop a tool ... what data is being consumed by a certain process, ... to build an automated test scenario.", which means, this will be used during a development and/or test phase to gather informations about the touched globals (for automated tests). So the performance is not an issue.
 
Second, the suggestion of Paul Waterman can always run, assuming the process runs with the required right and flag. One can always provide the required conditions.

Is my assumption correct, you use for the property "PrijsAkCatVm" an own data type (Asci.Getal) instead of %Numeric, in order to be able to use some extra parameters like HINT, VIEW, VISIBLE etc.
Is that correct? If yes, then there is a much simpler solution where neither  MS nor SQL won't stumble over it. The catchword is PropertyClass.

/// Create a new Cache class.
/// Define here all the extra parameter you need.
///
Class Extra.Props
{
    Parameter HINT;
    Parameter VIEW = 1;
    Parameter VISIBLE = 1;
    Parameter VOLGORDE;
    // etc.
}

/// then take your Cache Table, add the above defined class
/// to your Cache Table, complete the property definitions with
/// the appropriate parameters - Done.
///
Class Your.CacheTable Extends %Persistent [ PropertyClass = Extra.Props ]
{
   Property PrijsAkCatVm As %Numeric (CAPTION="...", "HINT=...", etc.); 
}

Now you can use the above defined (extra) parameters in each and every property  in this class.

There are two aspects of the question,
a) how to get the old property value of a modified property and
b) what happens, if one still opens the same object in the SAME PROCESS once again.

For a) the answer is already given by either using the GetStored() method or cloning the object. In cases, where an old propvalue is needed, I prefer the object cloning, especially if the base object contains embedded objects.
Of course, if the application uses the %Reload() method, then the %OnReload() callback method should do the same as %OnOpen() does.

Class DC.Data Extends %Persistent
{
Property Name As %String;
Property Addr As DataAddr;
Property oldObj As Data [ ReadOnly, Transient ];
Method %OnOpen() As %Status [ Private, ServerOnly = 1 ]
{
    set r%oldObj=..%ConstructClone(1)  // do a deep clone
    do ..%SetModified(0)               // clear modified state
    Quit $$$OK
}
}

Class DC.DataAddr Extends %SerialObject
{
Property Street As %String;
Property City As %String;
}

kill
set obj=##class(DC.Data).%New(),obj.Name="Joe"
set obj.Addr.Street="Abbey Rd. 123", obj.Addr.City="London"
write obj.%Save() --> 1

kill
set obj=##class(DC.Data).%OpenId(1)

set obj.Name="Paul"
write obj.Name --> Paul
write obj.oldObj.Name --> Joe

set obj.Addr.City="London East"
write obj.Addr.City --> London East
write obj.oldObj.Addr.City --> London

For b), it was said, that a second open (of the same object) returns the same OID. This is true. But there is one more thing. That second open doesn't even trys to get that object from the database but just keeps a lookout in the memory. This means two things (assuming no locks are used)
1) if the object was modified by another process, you "open the old version" already in the memory of your process, and
2) nevertheless that an other process deletes that object, your proces will open a nonexisting object...

Seq  Process-A                                  Process-B
-------------------------------------------------------------------------
 1   set obj1=##class(DC.Data).%OpenId(1)
 2                                              do ##class(DC.Data).%DeleteId(1)
 3   write ##class(DC.Data).%ExistsId(1) --> 0
 4   set obj2=##class(DC.Data).%OpenId(1)
 5   write obj1=obj2 --> 1
 

As a consequence of the above, I have learned not to use a simple open:

set obj=##class(Some.Class).%OpenId(oid)

but a more secure open, by invalidating a possible old instance:

set obj="", obj=##class(Some.Class).%OpenId(oid)

Nice, the first part of the problem (or project, if you wish) is solved. What about a "side project", not pinging all but the active VPNs only? Use the netstat command to get all the established connections

/// Execute an arbitrary OS command and return the command output
ClassMethod OSCmd(cmd, ByRef ans)
{
    kill ans
    open cmd:"QR":10
    set old=$system.Process.SetZEOF(1), ans=0
    if $test {
        use cmd
        for {read line quit:$zeof  set ans($increment(ans))=line}
        close cmd
    }
    quit ans
}

Of course, you have to take into account OS specific differences and access rights

do ##class(your.classname).OSCmd("ping google.com", .ans) zw ans
ans=11
ans(1)=""
ans(2)="Pinging google.com [142.250.185.174] with 32 bytes of data:"
ans(3)="Reply from 142.250.185.174: bytes=32 time=21ms TTL=114"
ans(4)="Reply from 142.250.185.174: bytes=32 time=27ms TTL=114"
ans(5)="Reply from 142.250.185.174: bytes=32 time=19ms TTL=114"
ans(6)="Reply from 142.250.185.174: bytes=32 time=18ms TTL=114"
ans(7)=""
ans(8)="Ping statistics for 142.250.185.174:"
ans(9)="    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),"
ans(10)="Approximate round trip times in milli-seconds:"
ans(11)=""

do ##class(your.classname).OSCmd("netstat -ano | findstr ""ESTABLISHED""", .ans) zw ans
ans=6
ans(1)="  TCP    127.0.0.1:23           127.0.0.1:50814        ESTABLISHED     8272"
ans(2)="  TCP    127.0.0.1:23           127.0.0.1:60874        ESTABLISHED     2872"
ans(3)="  TCP    127.0.0.1:23           127.0.0.1:63199        ESTABLISHED     8272"
ans(4)="  TCP    127.0.0.1:1972         127.0.0.1:49436        ESTABLISHED     2404"
ans(5)="  TCP    127.0.0.1:1972         127.0.0.1:51840        ESTABLISHED     2404"
ans(6)="  TCP    127.0.0.1:1972         127.0.0.1:54330        ESTABLISHED     2404"

Now you can analyse and process the command output...

Also, see this post too.

Four short lines of Objectscript code


ClassMethod Ping(host)
{
	set cmd="ping "_host
	open cmd:"QR":10
	for {use cmd read ans quit:$zeof  use 0 write ans,!}
	close cmd
}

// some test
do ##class(your.class).Ping("google.com")

Pinging google.com [142.250.185.110] with 32 bytes of data:
Reply from 142.250.185.110: bytes=32 time=25ms TTL=114
Reply from 142.250.185.110: bytes=32 time=26ms TTL=114
Reply from 142.250.185.110: bytes=32 time=19ms TTL=114
Reply from 142.250.185.110: bytes=32 time=19ms TTL=114

Ping statistics for 142.250.185.110:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 19ms, Maximum = 26ms, Average = 22ms