avatarTeri Radichel

Summary

The provided content discusses the importance of abstraction in software development to reduce code duplication, manage complexity, and enhance security by minimizing potential bugs.

Abstract

The article emphasizes the principle of abstraction in software engineering as a means to decrease the likelihood of security flaws in applications. It illustrates how abstracting common code into reusable components can lead to fewer errors and more maintainable systems. The author, Teri Radichel, shares personal experiences where refactoring code using abstraction saved time and reduced repetitive errors. She provides examples of Python code that abstract the process of pretty-printing JSON data, which is particularly useful for ad hoc queries of cloud configurations. The article also touches on the broader application of this principle in cybersecurity, suggesting that centralizing risky system components can improve their correctness and monitoring. Radichel encourages developers to review their code for duplication and common functionality that could benefit from abstraction, thereby reducing lines of code and potential security vulnerabilities.

Opinions

  • The author believes that every line of code is a potential source of bugs and that reducing the amount of code through abstraction can lead to more secure applications.
  • Teri Radichel advocates for the DRY (Don't Repeat Yourself) principle and abstraction as powerful techniques to solve cybersecurity problems.
  • She points out that repeated code is harder to manage and can propagate the same bugs throughout a codebase.
  • The author suggests that refactoring code to abstract out common functionality is an investment that pays off in the long run, despite the initial time cost.
  • Radichel emphasizes the importance of having an experienced team manage complex components like authentication and authorization to prevent mistakes by less-experienced developers.
  • She acknowledges that while her pretty-printing code example may not be perfect for all use cases, it serves as a practical demonstration of the benefits of abstraction and can be adapted as needed.
  • The author expresses that by abstracting code, she has significantly reduced type checking errors and no longer needs to remember how to handle different data types when parsing JSON.
  • Radichel encourages readers to apply the principles of DRY and abstraction in their own coding practices to reduce potential bugs and security vulnerabilities.

Every Line of Code is a Potential Bug

How to reduce the chances of a security flaw in your application with the principle of abstraction

One of my post that may later become a book on Secure Code. Also one of my posts on Application Security.

Free Content on Jobs in Cybersecurity | Sign up for the Email List

Sometimes I write a finding on a cloud and web application penetration test and then later realize that I need to be more specific about the finding I’ve described and how to fix it. The principles I use when writing software seem obvious to me, but I’ve been doing it so long. I have to remember that what is clear in my mind may not be so in the mind of others. I learn how to provide better information each time I deliver a report because of the questions clients ask me about the findings. Recent experiences led me to this series of posts.

As I was thinking about this new series, a security incident occurred related to some open-source software: faker.js and colors.js. This problem fits nicely into some of the things I was planning to write about anyway, so I’ll probably expand on that in a future post. I just found colors.js on a recent customer penetration test. I always research the included libraries in customer applications and provide guidance related to those libraries based on the information I discover.

Ironically, now we’ve had an incident with that code. My guidance may have proved concretely beneficial in that case with a real-world example. I’m hoping it did not affect my client, but on a penetration test, I don’t inspect their source control system or deployment pipeline. That’s something I do on cloud and product assessments upon request.

Core Principles

When I wrote my book on cybersecurity fundamentals at an executive level, I didn’t want to write about the technology of the day. I wanted to write about principles that could be applied to any cybersecurity program or problem. I explained how these same security principles apply in the cloud, even though the specifics of how the principles get applied change with different types of technology.

Some of the principles that form my thoughts on cybersecurity come from software engineering architecture. I’m not talking about programming language syntax, but concepts that apply regardless of which language you use. I’ve used a few, as noted on a recent Twitter post and this horribly outdated programming resume. It’s missing quite a few cybersecurity certifications, speaking and training engagements, and projects, some of which I cannot disclose. But it shows some of the projects and technologies I’ve used in the past.

Please, recruiters, look up LinkedIn profiles, and read them, before sending job descriptions.

Having worked in a myriad of industries with different technologies, programming languages, and system architectures, I tend to think about concepts that apply universally when designing and developing systems. I also think about how cybersecurity principles can apply above and beyond a specific technology or application. You still need to get into the specifics of a particular technology when it comes to implementation, but the principles apply regardless.

Abstraction

Abstraction, as it applies to writing software, is a means of reducing duplicate code in a software program. Another term I talk about in my cloud security classes is the DRY principle. Do Not Repeat Yourself. If you see yourself repeating the same lines of code over and over again, you have to consider whether you have a design problem. This same principle applies when you are writing infrastructure code such as AWS CloudFormation.

One example of abstraction is the use of the Abstract Factory Pattern to create a common set of code to create new objects. You can pull out the repetitive code that creates an object and move it to a single, reusable code base. Then you only have to write the code that changes for each type of object you want to create.

I talk about how to apply the principle of abstraction to governance and cloud architectures in my cloud security classes. Abstraction is a very powerful technique when trying to solve cybersecurity problems.

What are the problems with repetitious code?

I remember working on an e-commerce site for a regional auto-parts retailer. A programmer working on the project had copied and pasted a particular block of code all over the website. As it turns out, that particular piece of code had a bug in it. Before I realized what the programmer had done, I fixed a bug in that code. Then the error message appeared again, which confused me for a minute. Then I realized that the same buggy code was all over the website, and I had to hunt down every instance of it. Great.

Alternatively, if the developer had written one reusable class for that code used in multiple places, I could have fixed that code one time, and the problem would have been fixed everywhere. That’s the problem with repetitive code. It’s harder to manage, and as the title says, every line of code is a potential bug. Fewer lines of code generally means fewer bugs.

Here’s another problem with repetitive code. You have multiple people across the organization writing the same risky code. You’re counting on every single individual to do the right thing and not make a mistake. You’re counting on different QA teams to test that code and ensure that there are no flaws in it. Different individuals on different teams have more or less training or engineering skills to know how to correctly and securely implement that component.

I often talk to clients on IANS Research calls about their system architectures. I explain that abstracting out risky system components like authentication and authorization into a single component can help ensure they are correct, resilient, and more easily monitored. Have an experienced team write and manage that portion of your architecture rather than hoping that none of the less-experienced developers writing similar complex code across your organization make a mistake. The same concept applies to other parts of system architecture as well.

Common errors can also become repetitive in code. By abstracting out common functionality you can handle certain errors consistently. You can write appropriate error messages and prevent certain flaws before they proliferate throughout your code base.

Maybe I’ll get to some caveats that exist when leveraging abstraction in a separate post. As with anything, architects and engineers need to balance different principles when coming up with robust solutions, including system and software architecture. In this article, I’m going to focus on abstraction alone and provide a concrete example.

Pretty Printing JSON nodes

I have tools that query AWS to answer particular questions such as one I wrote about in the past — mapping network attack paths. I also review some configurations using a manual spot check along with automation to see if I find anything that looks risky. If I’m checking things manually I’ll often want to review all the data for a set of resources.

When you call the AWS APIs using the boto3 Python SDK, you often get back a blob of JSON with attributes describing a particular AWS resource or list of resources. The blob of data you get back is a bit hard to read.

For example, I could write a function like this:

def dump_json_ec2():
profile="default"
    service="ec2"
    region="us-east-1"
    session=Session(profile)
    client=session.get_client(service, region)
    all_instances = client.describe_instances()
    for reservation in all_instances["Reservations"]:
        for instance in reservation["Instances"]:
            print(instance)

It prints the following. I truncated the following but you get the idea. Also, these resources no longer exist. :-)

{‘AmiLaunchIndex’: 0, ‘ImageId’: ‘ami-08e4e35cccc6189f4’, ‘InstanceId’: ‘i-0f38d73235b66af78’, ‘InstanceType’: ‘t2.micro’, ‘LaunchTime’: datetime.datetime(2022, 1, 10, 22, 29, 23, tzinfo=tzlocal()), ‘Monitoring’: {‘State’: ‘disabled’}, ‘Placement’: {‘AvailabilityZone’: ‘us-east-1c’, ‘GroupName’: ‘’, ‘Tenancy’: ‘default’}, ‘PrivateDnsName’: ‘ip-1723191181.ec2.internal’, ‘PrivateIpAddress’: ‘172.31.91.181’, ‘ProductCodes’: [], ‘PublicDnsName’: ‘ec2–174129182241.compute-1.amazonaws.com’, ‘PublicIpAddress’: ‘174.129.182.241’, ‘State’: {‘Code’: 16, ‘Name’: ‘running’}, ‘StateTransitionReason’: ‘’, ‘SubnetId’: ‘subnet-0b1a01d131ef3ecbd’, ‘VpcId’: ‘vpc-00ac81543e1520b38’, ‘Architecture’: ‘x86_64’, ‘BlockDeviceMappings’: [{‘DeviceName’: ‘/dev/xvda’, ‘Ebs’: {‘AttachTime’: datetime.datetime(2022, 1, 10, 22, 29, 24, tzinfo=tzlocal()), ‘DeleteOnTermination’: True, ‘Status’: ‘attached’, ‘VolumeId’: ‘vol-010072528f5cff8ee’}}], ‘ClientToken’: ‘’, ‘EbsOptimized’: False, ‘EnaSupport’: True, ‘Hypervisor’: ‘xen’, ‘NetworkInterfaces’: [{‘Association’: {‘IpOwnerId’: ‘amazon’, ‘PublicDnsName’: ‘ec2–174129182241.compute-1.amazonaws.com’, ‘PublicIp’: ‘174.129.182.241’}, ‘Attachment’: {‘AttachTime’: datetime.datetime(2022, 1, 10, 22, 29, 23, tzinfo=tzlocal()), ‘AttachmentId’: ‘eni-attach-0cdb512d994c052cb’, ‘DeleteOnTermination’: True, ‘DeviceIndex’: 0, ‘Status’: ‘attached’, ‘NetworkCardIndex’: 0}, ‘Description’: ‘’, ‘Groups’: [{‘GroupName’: ‘default’, ‘GroupId’: ‘sg-09373537d404bdabf’}], 
[... truncated...]
‘DeleteOnTermination’: True, ‘DeviceIndex’: 0, ‘Status’: ‘attached’, ‘NetworkCardIndex’: 0}, ‘Description’: ‘’, ‘Groups’: [{‘GroupName’: ‘default’, ‘GroupId’: ‘sg-09373537d404bdabf’}], ‘Ipv6Addresses’: [], ‘MacAddress’: ‘12:8c:bf:29:df:bd’, ‘NetworkInterfaceId’: ‘eni-06a3a7215f49231cc’, ‘OwnerId’: ‘xxxxxxxxxxxx’, ‘PrivateDnsName’: ‘ip-172318864.ec2.internal’, ‘PrivateIpAddress’: ‘172.31.88.64’, ‘PrivateIpAddresses’: [{‘Association’: {‘IpOwnerId’: ‘amazon’, ‘PublicDnsName’: ‘ec2–23236151.compute-1.amazonaws.com’, ‘PublicIp’: ‘23.23.6.151’}, ‘Primary’: True, ‘PrivateDnsName’: ‘ip-172318864.ec2.internal’, ‘PrivateIpAddress’: ‘172.31.88.64’}], ‘SourceDestCheck’: True, ‘Status’: ‘in-use’, ‘SubnetId’: ‘subnet-0b1a01d131ef3ecbd’, ‘VpcId’: ‘vpc-00ac81543e1520b38’, ‘InterfaceType’: ‘interface’}], ‘RootDeviceName’: ‘/dev/sda1’, ‘RootDeviceType’: ‘ebs’, ‘SecurityGroups’: [{‘GroupName’: ‘default’, ‘GroupId’: ‘sg-09373537d404bdabf’}], ‘SourceDestCheck’: True, ‘Tags’: [{‘Key’: ‘Name’, ‘Value’: ‘Test-Instance-2’}], ‘VirtualizationType’: ‘hvm’, ‘CpuOptions’: {‘CoreCount’: 1, ‘ThreadsPerCore’: 1}, ‘CapacityReservationSpecification’: {‘CapacityReservationPreference’: ‘open’}, ‘HibernationOptions’: {‘Configured’: False}, ‘MetadataOptions’: {‘State’: ‘applied’, ‘HttpTokens’: ‘optional’, ‘HttpPutResponseHopLimit’: 1, ‘HttpEndpoint’: ‘enabled’, ‘HttpProtocolIpv6’: ‘disabled’}, ‘EnclaveOptions’: {‘Enabled’: False}, ‘PlatformDetails’: ‘Windows’, ‘UsageOperation’: ‘RunInstances:0002’, ‘UsageOperationUpdateTime’: datetime.datetime(2022, 1, 10, 22, 35, 59, tzinfo=tzlocal())}

I wanted to make that more readable for manual review. While writing my queries initially, I started writing code to print out the attributes of each resource in a more readable format. For example, I might write this code to make EC2 instance output more readable:

def no_abstraction_ec2():
profile="default"
    service="ec2"
    region="us-east-1"
    session=Session(profile)
    client=session.get_client(service, region)
    all_instances = client.describe_instances()
    for reservation in all_instances["Reservations"]:
        for instance in reservation["Instances"]:
            print("\t" + instance["InstanceId"])
            for tag in instance["Tags"]:
                if tag["Key"]=="Name": print ("\t\t" + tag["Value"])
                for sg in instance["SecurityGroups"]:
                    sgid=sg["GroupId"]
                    print ("\t\t" + sgid + " " + sg["GroupName"])
#This is only a subset of the available available attributes
#Would need to write a lot more code to get all the attributes
#In the full JSON blob returned by describe_instances

With all that code, I am able to retrieve the following data related to ec2 instances in that region of my account:

i-0f38d73235b66af78
  Test-Instance-1
  sg-09373537d404bdabf default
i-07a4274d5d0a88019
  Test-Instance-3
  sg-09373537d404bdabf default
i-09a0bde11b69bd96e
  Test-Instance-2
  sg-09373537d404bdabf default

In order to get all the data in that JSON response above I would need to write a lot more lines of code similar to the above to print every value.

Anytime you see repeated code, you should to ask yourself if you could possibly write a more elegant solution.

Almost immediately, I noticed I was repeating lines of code that looked very similar to each other for each different resource I wanted to review.

You can see in the sample code that I am printing \t repeatedly to get a tab on certain lines. I have to loop through attributes with multiple values. I would need to write a line of code for every available attribute if I wanted to print them all.

Depending on your application requirements, you may be forced to retrieve and print individual attribute values. Sometimes I do as well. But at least when writing ad hoc queries of cloud configuration I simply need to make any blob of JSON more readable. I want to print every attribute. That means I can abstract out the traversal and printing of each attribute of the JSON response.

You can also see that the profile and region would need to be updated to get data from different sources. That could easily lead to repetitive code. I wrote a separate blog post about some reusable code I wrote to obtain a refreshable session, which which I’m going to use in my example below. Leveraging the code in refreshsession.py will eliminate some lines of code related to obtaining and refreshing sessions and updating the client to use different profiles and regions.

Repeatedly getting the same errors may indicate room for abstraction in your code base.

The other thing I noticed is that I was generating the same errors over and over again. When I tried to query an attribute, was it a list, a dictionary, a single value, or something else? If I selected the wrong type it would throw various errors, some of which were not exactly obvious as to the specific problem. I would need to review the JSON to ensure I was retrieving the value for the correct type. Even when I did that I was still making mistakes.

Noticing those two aspects of my code and error messages led me to spend a bit of time abstracting out the common code into a reusable block of python code. Instead of grepping the JSON blob to determine what type of value I was trying to print and the key names for each attribute, I wrote reusable code that evaluates the node I pass in to determine the type. It loops through and prints out any key names and values. It also inserts tabs appropriately to help make the resources more readable.

I put this code in a file called printer.py:

#!/usr/bin/python3
def printdict(d,tabs):
    for v in d: printvalue(v,d[v],tabs + '\t')
    return
def printlist(l,tabs):
    for i, v in enumerate(l): printvalue(l,v,tabs + '\t')
    return
def printrootvalue(k):
    printvalue(k, None, "")
def printvalue(k,v,tabs):
if v is None:
            if isinstance(k,list):printlist(k,tabs);return
            if isinstance(k,dict):printdict(k,tabs);return
if isinstance(k,str):
            if not isinstance(v,str):
                if isinstance(v,list) or isinstance(v,dict):
                    if (len(v) > 0):
                        print(tabs + k + ": ")
                        if isinstance(v,list):printlist(v,tabs);return
                        if isinstance(v,dict):printdict(v,tabs);return
if (len(str(v))) > 0 and str(v) != '[]' and v != None:
                print(tabs + k.strip() + ": " + str(v).strip());
            #else:
                #print(k)
            return
if isinstance(v,str):
           if not isinstance(k,list):
              if isinstance(v,dict): printdict(v,tabs);return
              if isinstance(v,list): printlist(v,tabs);return
              print(k + ":")
              if isinstance(k[v], dict):printdict(k[v],tabs); return
              if isinstance(k[v], list):printlist(k[v],tabs); return
           else: print(tabs + v);return;
if isinstance(v,dict):
          printdict(v,tabs);return;
print(k);print(v);
      print("unandled k,v type:"); print(type(k));print(type(v));return;

Now instead of writing a single line of code for every attribute I want to retrieve for a resource, I can write code like this:

def abstraction_ec2_instances(client):
    all_instances = client.describe_instances()
    printer.printrootvalue(all_instances)

The printer code can figure out what the attributes are and their types. It will insert tabs in the appropriate places. The new output from that one function call includes all the attributes and looks something like this (truncated, but you get the idea):

Reservations: 
    Instances: 
        AmiLaunchIndex: 0
        ImageId: ami-08e4e35cccc6189f4
        InstanceId: i-0f38d73235b66af78
        InstanceType: t2.micro
        LaunchTime: 2022-01-10 22:29:23+00:00
        Monitoring: 
          State: disabled
        Placement: 
          AvailabilityZone: us-east-1c
          Tenancy: default
        PrivateDnsName: ip-172-31-91-181.ec2.internal
        PrivateIpAddress: 172.31.91.181
        PublicDnsName: ec2-174-129-182-241.compute-1.amazonaws.com
        PublicIpAddress: 174.129.182.241
        State: 
          Code: 16
          Name: running
        SubnetId: subnet-0b1a01d131ef3ecbd
        VpcId: vpc-00ac81543e1520b38
        Architecture: x86_64
        BlockDeviceMappings: 
            DeviceName: /dev/xvda
            Ebs: 
              AttachTime: 2022-01-10 22:29:24+00:00
              DeleteOnTermination: True
              Status: attached
              VolumeId: vol-010072528f5cff8ee
        EbsOptimized: False
        EnaSupport: True
        Hypervisor: xen
        NetworkInterfaces: 
            Association: 
              IpOwnerId: amazon
              PublicDnsName: ec2-174-129-182-241.compute-1.amazonaws.com
              PublicIp: 174.129.182.241
            Attachment: 
              AttachTime: 2022-01-10 22:29:23+00:00
              AttachmentId: eni-attach-0cdb512d994c052cb
              DeleteOnTermination: True
              DeviceIndex: 0
              Status: attached
              NetworkCardIndex: 0
            Groups: 
                GroupName: default
                GroupId: sg-09373537d404bdabf
            MacAddress: 12:7c:17:74:c7:73
            NetworkInterfaceId: eni-013365105ed089229
            OwnerId: xxxxxxxxxxxxx
            PrivateDnsName: ip-172-31-91-181.ec2.internal
            PrivateIpAddress: 172.31.91.181
            PrivateIpAddresses: 
                Association: 
                  IpOwnerId: amazon
                  PublicDnsName: ec2-174-129-182-241.compute-1.amazonaws.com
                  PublicIp: 174.129.182.241
                Primary: True
                PrivateDnsName: ip-172-31-91-181.ec2.internal
                PrivateIpAddress: 172.31.91.181
            SourceDestCheck: True
            Status: in-use
            SubnetId: subnet-0b1a01d131ef3ecbd
            VpcId: vpc-00ac81543e1520b38
            InterfaceType: interface
        RootDeviceName: /dev/xvda
        RootDeviceType: ebs
        SecurityGroups: 
            GroupName: default
            GroupId: sg-09373537d404bdabf
        SourceDestCheck: True
        Tags: 
            Key: Name
            Value: Test-Instance-1
        VirtualizationType: hvm
        CpuOptions: 
          CoreCount: 1
          ThreadsPerCore: 1
        CapacityReservationSpecification: 
          CapacityReservationPreference: open
        HibernationOptions: 
          Configured: False
        MetadataOptions: 
          State: applied
          HttpTokens: optional
          HttpPutResponseHopLimit: 1
          HttpEndpoint: enabled
          HttpProtocolIpv6: disabled
        EnclaveOptions: 
          Enabled: False
        PlatformDetails: Linux/UNIX
        UsageOperation: RunInstances
        UsageOperationUpdateTime: 2022-01-10 22:29:23+00:00
    OwnerId: xxxxxxxxxxxx
    ReservationId: r-03ca3afc8d1f555a1
    Instances: 
        AmiLaunchIndex: 0
        ImageId: ami-083602cee93914c0c
        InstanceId: i-07a4274d5d0a88019
        InstanceType: t2.micro
.....

I can use that same reusable code to print out nodes within a JSON response. For instance, when you request the JSON for Security Groups, it has a node “SecurityGroups.” I want to pretty print that node without the MetaData node at the same level. I can use this code:

def abstraction_security_groups(client):
    all_sgs = client.describe_security_groups()
    printer.printrootvalue(all_sgs['SecurityGroups'])

That now prints out all the data associated with the SecurityGroups attribute in the JSON response. In this example, I only have the one default security group in this region of this account. (Best practice to delete that but this is just for demo purposes.)

Description: default VPC security group
GroupName: default
IpPermissions: 
    IpProtocol: -1
    UserIdGroupPairs: 
        GroupId: sg-09373537d404bdabf
        UserId: xxxxxxxxxxxx
OwnerId: xxxxxxxxxxxx
GroupId: sg-09373537d404bdabf
IpPermissionsEgress: 
    IpProtocol: -1
    IpRanges: 
        CidrIp: 0.0.0.0/0
VpcId: vpc-00ac81543e1520b38

Refactoring Code

Refactoring is the process of revising your code to make it cleaner, clearer, more efficient, or just plain better.

The point of this is not to show you my awesome code. I am sure better pretty printers exist for JSON. I wrote this quickly to do ad hoc queries on cloud resources for reports. I am also using it as sample code to demonstrate a concept.

The point of showing you this code is to demonstrate how to abstract out common code into a class that performs the work that happens repeatedly in an application, instead of copying and pasting and making the same mistakes over and over again in your code.

I am the queen of typos and am often amazed at how often I can make the same mistake. I also get syntax mixed up at times because I do a lot of context-switching. I may be writing a quick script in bash, then switching to Python and then to Ruby, .Net, or PHP for an application I’m pentesting. Any chance I get to abstract away my typos and mistakes — I’ll take it!

I also forget things because it seems like I’m always doing something new and then having to repeat something I did in the past that I now can’t remember. If I write a program to do it for me, I can reuse it in the future and get my work done much faster. Sometimes it takes a bit more time to write reusable code, but in the end it always pays off.

I’ve made a lot of use of this pretty-printing code. It has saved me a ton of time (more than it took me to revise and abstract the common code.) I can quickly review any configurations from APIs that return JSON.

Feel free to use this code if you want, but no warranties. Depending on your use case, you may want to add additional checks on any data inserted by clients and more robust error handling. In my case there’s low risk that I’m going to insert bad data into my own code to attack my own systems, so I can be a little looser with the security checks and error handling.

The other benefit of this refactored code is that I have pretty much eliminated type checking errors when I use that function. The code checks to see if it is printing a dictionary, list, or value and handles the attribute appropriately. No more me trying to guess what type it is or digging through unreadable JSON to try to figure out what it is. I also don’t have to remember how to handle each different type if I want to obtain the value and print it out, something that kept throwing me for a loop when I first started parsing JSON files.

Next steps?

How can you put the principles of DRY and abstraction into use in your code?

  • Review and consider where your code has duplicated code, common functionality, and repetitive error messages to determine if some refactoring may be in order.
  • Abstract out common functionality to reduce lines of code.
  • Create a standard way to handle common errors.
  • Move risky system functionality into common components into a single code base, written by developers who understand and follow software security best practices.

Hopefully, that will help you reduce potential bugs and security vulnerabilities at the same time.

In my next post I’m going to show you how to apply this principle to some sample open source code.

Follow for updates.

Teri Radichel | © 2nd Sight Lab 2021

About Teri Radichel:
~~~~~~~~~~~~~~~~~~~~
⭐️ Author: Cybersecurity Books
⭐️ Presentations: Presentations by Teri Radichel
⭐️ Recognition: SANS Award, AWS Security Hero, IANS Faculty
⭐️ Certifications: SANS ~ GSE 240
⭐️ Education: BA Business, Master of Software Engineering, Master of Infosec
⭐️ Company: Penetration Tests, Assessments, Phone Consulting ~ 2nd Sight Lab
Need Help With Cybersecurity, Cloud, or Application Security?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
🔒 Request a penetration test or security assessment
🔒 Schedule a consulting call
🔒 Cybersecurity Speaker for Presentation
Follow for more stories like this:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
❤️ Sign Up my Medium Email List
❤️ Twitter: @teriradichel
❤️ LinkedIn: https://www.linkedin.com/in/teriradichel
❤️ Mastodon: @teriradichel@infosec.exchange
❤️ Facebook: 2nd Sight Lab
❤️ YouTube: @2ndsightlab
Software Security
Application Security
Abstraction
Cybersecurity
Cloud Security
Recommended from ReadMedium