diff --git a/build_whl.com b/build_whl.com
new file mode 100644
index 0000000000000000000000000000000000000000..a152158c3b479f42ba0afe941983380df9164082_YnVpbGRfd2hsLmNvbQ==
--- /dev/null
+++ b/build_whl.com
@@ -0,0 +1,3 @@
+$ python -m build -w -n -x -o ./ ./
+$
+$ exit
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..a152158c3b479f42ba0afe941983380df9164082_cHlwcm9qZWN0LnRvbWw=
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,39 @@
+[build-system]
+requires = [
+    "setuptools>=42",
+    "wheel"
+]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "secrules"
+version = "0.1.0"
+description = "secrules for VSI Python 3"
+readme = "README.md"
+requires-python = ">= 3.10"
+license.file = "LICENSE"
+authors = [
+  { name = "Jean-François Piéronne", email = "jf.pieronne@laposte.net" },
+]
+classifiers = [
+  "License :: OSI Approved ::  GNU LGPL v3 License",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3 :: Only",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: Implementation :: CPython",
+]
+urls.homepage = "https://foss.vmsgenerations.org/openvms/tools/secrules/"
+urls.changelog = "https://foss.vmsgenerations.org/openvms/tools/secrules/"
+
+dependencies = [
+]
+
+[project.optional-dependencies]
+docs = [
+]
+test = [
+]
+typing = [
+]
+virtualenv = [
+]
diff --git a/secrules/__main__.py b/secrules/__main__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a152158c3b479f42ba0afe941983380df9164082_c2VjcnVsZXMvX19tYWluX18ucHk=
--- /dev/null
+++ b/secrules/__main__.py
@@ -0,0 +1,164 @@
+# -*- coding: iso-8859-1 -*-
+
+import sys
+import os
+import re
+import argparse
+import importlib
+
+DEBUG = False
+if DEBUG:
+    import debugpy
+
+    # 5678 is the default attach port in the VS Code debug configurations.
+    # Unless a host and port are specified, host defaults to 127.0.0.1
+    debugpy.configure(subProcess=False)
+
+    debugpy.listen(('0.0.0.0', 5678), in_process_debug_adapter=True)
+    print('Waiting for debugger attach')
+    debugpy.wait_for_client()
+    debugpy.breakpoint()
+    print('break on this line')
+
+
+all_rules = {}
+args = None
+
+
+def rules_exec(seclass, numrule=None, info=False, fo=None, export=None):
+    global all_rules, args
+    rules = all_rules[seclass][1]
+    m = all_rules[seclass][0]
+    if numrule is None:
+        for r in rules:
+            if info:
+                print(getattr(m, r).__name__)
+                print(getattr(m, r).__doc__)
+                print()
+            else:
+                getattr(m, r)(fo, export)
+    else:
+        for n in numrule:
+            rname = 'rule%s%02d' % (seclass[-2:], n)
+            if rname in rules:
+                if info:
+                    print(getattr(m, rname).__name__)
+                    print(getattr(m, rname).__doc__)
+                    print()
+                else:
+                    getattr(m, rname)(fo, export)
+
+
+class InflateRange(argparse.Action):
+    def __call__(self, parser, namespace, values, option_string=None):
+        lst = []
+        for string in values:   # type: ignore
+            string = string.replace('(', '')
+            string = string.replace(')', '')
+            if '-' in string or ':' in string:
+                string = string.replace(':', '-')
+                m = re.match(r'(\d+)(?:-(\d+))?$', string)
+                # ^ (or use .split('-'). anyway you like.)
+                if not m:
+                    raise argparse.ArgumentTypeError(
+                        "'"
+                        + string
+                        + "' is not a range of number. Expected forms like '0-5' or '2'."
+                    )
+                start = m.group(1)
+                end = m.group(2) or start
+                lst.extend(list(range(int(start, 10), int(end, 10) + 1)))
+            else:
+                string = string.replace(',', ' ')
+                for string in string.split(' '):
+                    if string:
+                        lst.append(int(string))
+            setattr(namespace, self.dest, lst)
+
+
+def load_rules(levels):
+    global all_rules
+    mods = [
+        fn[:-3]
+        for fn in os.listdir('./secrules')
+        if fn.startswith('rule') and fn[-1:].lower() == 'y'
+    ]
+    all_rules = {}
+    for modn in mods:
+        m = importlib.import_module('.' + modn, 'secrules')
+        # m = __import__('secrules.' + modn, globals(), locals(), ['*'], -1)
+        lst = [m, []]
+        for r in dir(m):
+            if r.startswith('rule'):
+                if (
+                    levels is None
+                    or not hasattr(getattr(m, r), 'rule_level')
+                    or getattr(m, r).rule_level in levels
+                ):
+                    lst[1].append(r)
+        all_rules[modn] = lst
+        # all_rules[modn] = (m, [r for r in dir(m) if r.startswith('rule')])
+
+
+def main():
+    global args
+    parser = argparse.ArgumentParser(description='security checker')
+    parser.add_argument(
+        '--output',
+        type=argparse.FileType('w'),
+        dest='fo',
+        metavar='out-file',
+        help='output file',
+        default=sys.stdout,
+    )
+    parser.add_argument(
+        '--class', type=int, dest='seclass', help='security class'
+    )
+    parser.add_argument(
+        '--rule',
+        action=InflateRange,
+        nargs='*',
+        dest='numrule',
+        help='rule number',
+    )
+    parser.add_argument(
+        '--export',
+        action='store_true',
+        dest='export',
+        default=False,
+        help='export format',
+    )
+    parser.add_argument(
+        '--info',
+        action='store_true',
+        dest='info',
+        default=False,
+        help='Rules info',
+    )
+    parser.add_argument(
+        '--level',
+        action=InflateRange,
+        nargs='*',
+        dest='levels',
+        help='rule levels',
+    )
+
+    args = parser.parse_args()
+
+    load_rules(args.levels)
+
+    if args.seclass is None:
+        if args.numrule is not None:
+            raise argparse.ArgumentTypeError('missing seclass argument')
+        lst = list(all_rules.keys())
+        lst.sort()
+        for seclass in lst:
+            #            seclass = 'rules%02d' % args.seclass
+            rules_exec(seclass, args.numrule, args.info, args.fo, args.export)
+    else:
+        seclass = 'rules%02d' % args.seclass
+        rules_exec(seclass, args.numrule, args.info, args.fo, args.export)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..a152158c3b479f42ba0afe941983380df9164082_c2V0dXAucHk=
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,20 @@
+from setuptools import setup, find_packages, Distribution
+
+class PureDistribution(Distribution):
+    def is_pure(self):
+        return True
+
+    def has_ext_modules(self):
+        return False
+
+setup(
+    # this will be the package name you will see
+    name = 'secrules',
+    # some version number you may wish to add - increment this after every update
+    version='0.1',
+    package_data={'': ['*.exe', '*.pyi']},
+    distclass=PureDistribution,
+
+    packages=find_packages(), #include/exclude arguments take * as wildcard, . for any sub-package names
+)
+