Home > Sample chapters > Programming > Visual Studio and .NET

Design Principles and Patterns for Software Engineering with Microsoft .NET

Applying Requirements by Design

In Chapter 1. we saw that international standard ISO/IEC 9126 lists testability and security as key quality characteristics for any software architecture. This means that we should consider testability and security as nonfunctional requirements in any software architecture and start planning for them very early in the design phase.

Testability

A broadly accepted definition for testability in the context of software architecture describes it as the ease of performing testing. And testing is the process of checking software to ensure that it behaves as expected, contains no errors, and satisfies its requirements.

A popular slogan to address the importance of software testing comes from Bruce Eckel and reads like this:

  • If it ain’t tested, it’s broken.

The key thing to keep in mind is that you can state that your code works only if you can provide evidence for that it does. A piece of software can switch to the status of working not when someone states it works (whether stated by end users, the project manager, the customer, or the chief architect), but only when its correctness is proven beyond any reasonable doubt.

Software Testing

Testing happens at various levels. You have unit tests to determine whether individual components of the software meet functional requirements. You have integration tests to determine whether the software fits in the environment and infrastructure and whether two or more components work well together. Finally, you have acceptance tests to determine whether the completed system meets customer requirements.

Unit tests and integration tests pertain to the development team and serve the purpose of making the team confident about the quality of the software. Test results tell the team if the team is doing well and is on the right track. Typically, these tests don’t cover the entire code base. In general, there’s no clear correlation between the percentage of code coverage and quality of code. Likewise, there’s also no agreement on what would be a valid percentage of code coverage to address. Some figure that 80 percent is a good number. Some do not even instruct the testing tool to calculate it.

The customer is typically not interested in the results of unit and integration tests. Acceptance tests, on the other hand, are all the customer cares about. Acceptance tests address the completed system and are part of the contract between the customer and the development team. Acceptance tests can be written by the customer itself or by the team in strict collaboration with the customer. In an acceptance test, you can find a checklist such as the following one:

1) Insert a customer with the following data ...;

2) Modify the customer using an existing ID;

3) Observe the reaction of the system and verify specific expected results;

Another example is the following:

1) During a batch, shut down one nodes on the application server;

2) Observe the reaction of the system and the results of the transaction;

Run prior to delivery, acceptance tests, if successful, signal the termination of the project and the approval of the product. (As a consultant, you can issue your final invoice at this point.)

Tests are a serious matter.

Testing the system by having end users poke around the software for a few days is not a reliable (and exhaustive) test practice. As we saw earlier in the chapter, it is even considered to be an antipattern.

Software Contracts

A software test verifies that a component returns the correct output in response to given input and a given internal state. Having control over the input and the state and being able to observe the output is therefore essential.

Your testing efforts greatly benefit from detailed knowledge of the software contract supported by a method. When you design a class, you should always be sure you can answer the following three questions about the class and its methods in particular:

  • Under which conditions can the method be invoked?

  • Which conditions are verified after the method terminates?

  • Which conditions do not change before and after the method execution?

These three questions are also known, respectively, as preconditions, postconditions, and invariants.

Preconditions mainly refer to the input data you pass; specifically, data that is of given types and values falling within a given range. Preconditions also refer to the state of the object required for execution—for example, the method that might need to throw an exception if an internal member is null or if certain conditions are not met.

When you design a method with testability in mind, you pay attention to and validate input carefully and throw exceptions if any preconditions are not met. This gets you clean code, and more importantly, code that is easier to test.

Postconditions refer to the output generated by the method and the changes produced to the state of the object. Postconditions are not directly related to the exceptions that might be thrown along the way. This is not relevant from a testing perspective. When you do testing, in fact, you execute the method if preconditions are met (and if no exceptions are raised because of failed preconditions). The method might produce the wrong results, but it should not fail unless really exceptional situations are encountered. If your code needs to read a file, that the file exists is a precondition and you should throw a FileNotFoundException before attempting to read. A FileIOException, say, is acceptable only if during the test you lose connection to the file.

There might be a case where the method delegates some work to an internal component, which might also throw exceptions. However, for the purpose of testing, that component will be replaced with a fake one that is guaranteed to return valid data by contract. (You are testing the outermost method now; you have tested the internal component already or you’ll test it later.) So, in the end, when you design for testability the exceptions you should care about most are those in the preconditions.

Invariants refer to property values, or expressions involving members of the object’s state, that do not change during the method execution. In a design for testability scenario, you know these invariants clearly and you assert them in tests. As an example of an invariant, consider the property Status of DbConnection: it has to be Open before you invoke BeginTransaction, and it must remain Open afterward.

Software contracts play a key role in the design of classes for testability. Having a contract clearly defined for each class you write makes your code inherently more testable.

Unit Testing

Unit testing verifies that individual units of code are working properly according to their software contract. A unit is the smallest part of an application that is testable—typically, a method.

Unit testing consists of writing and running a small program (referred to as a test harness) that instantiates classes and invokes methods in an automatic way. In the end, running a battery of tests is much like compiling. You click a button, you run the test harness and, at the end of it, you know what went wrong, if anything.

In its simplest form, a test harness is a manually written program that reads test-case input values and the corresponding expected results from some external files. Then the test harness calls methods using input values and compares results with expected values. Needless to say, writing such a test harness entirely from scratch is, at the very minimum, time consuming and error prone. But, more importantly, it is restrictive in terms of the testing capabilities you can take advantage of.

At the end of the day, the most effective way to conduct unit testing passes through the use of an automated test framework. An automated test framework is a developer tool that normally includes a runtime engine and a framework of classes for simplifying the creation of test programs. Table 3-2 lists some of the most popular ones.

Table 3-2. Popular Testing Tools

Product

Description

MSTest

The testing tool incorporated into Visual Studio 2008 Professional, Team Tester, and Team Developer. It is also included in Visual Studio 2005 Team Tester and Team Developer.

MBUnit

An open-source product with a fuller bag of features than MSTest. However, the tight integration that MSTest has with Visual Studio and Team Foundation Server largely makes up for the smaller feature set. For more information on MBUnit, pay a visit to http://www.mbunit.com.

NUnit

One of the most widely used testing tools for the .NET Framework. It is an open-source product. Read more at http://www.nunit.org.

xUnit.NET

Currently under development as a CodePlex project, this tool builds on the experience of James Newkirk—the original author of NUnit. It is definitely an interesting tool to look at, with some interesting and innovative features. For more information, pay a visit to http://www.codeplex.com/xunit.

A nice comparison of testing tools, in terms of their respective feature matrix, is available at http://www.codeplex.com/xunit/Wiki/View.aspx?title=Comparisons.

Unit Testing in Action

Let’s have a look at some tests written using the MSTest tool that comes with Visual Studio 2008. You start by grouping related tests in a text fixture. Text fixtures are just test-specific classes where methods typically represent tests to run. In a text fixture, you might also have code that executes at the start and end of the test run. Here’s the skeleton of a text fixture with MSTest:

using Microsoft.VisualStudio.TestTools.UnitTesting;
.
.
.
[TestClass]
public class CustomerTestCase
{
  private Customer customer;

  [TestInitialize]
  public void SetUp()
  {
    customer = new Customer();
  }

  [TestCleanup]
  public void TearDown()
  {
    customer = null;
  }

  // Your tests go here
  [TestMethod]
  public void Assign_ID()
  {
    .
    .
    .
  }
  .
  .
  .
}

It is recommended that you create a separate assembly for your tests and, more importantly, that you have tests for each class library. A good practice is to have an XxxTestCase class for each Xxx class in a given assembly.

As you can see, you transform a plain .NET class into a test fixture by simply adding the TestClass attribute. You turn a method of this class into a test method by using the TestMethod attribute instead. Attributes such as TestInitialize and TestCleanup have a special meaning and indicate code to execute at the start and end of the test run. Let’s examine an initial test:

[TestMethod]
public void Assign_ID()
{
  // Define the input data for the test
  string id = "MANDS";

  // Execute the action to test (assign a given value)
  customer.ID = id;

  // Test the postconditions:
  // Ensure that the new value of property ID matches the assigned value.
  Assert.AreEqual(id, customer.ID);
}

The test simply verifies that a value is correctly assigned to the ID property of the Customer class. You use methods of the Assert object to assert conditions that must be true when checked.

The body of a test method contains plain code that works on properties and methods of a class. Here’s another example that invokes a method on the Customer class:

[TestMethod]
public void TestEmptyCustomersHaveNoOrders()
{
  Customer c = new Customer();
  Assert.AreEqual<decimal>(0, c.GetTotalAmountOfOrders());
}

In this case, the purpose of the test is to ensure that a newly created Customer instance has no associated orders and the total amount of orders add up to zero.

Dealing with Dependencies

When you test a method, you want to focus only on the code within that method. All that you want to know is whether that code provides the expected results in the tested scenarios. To get this, you need to get rid of all dependencies the method might have. If the method, say, invokes another class, you assume that the invoked class will always return correct results. In this way, you eliminate at the root the risk that the method fails under test because a failure occurred down the call stack. If you test method A and it fails, the reason has to be found exclusively in the source code of method A—given preconditions, invariants, and behavior—and not in any of its dependencies.

Generally, the class being tested must be isolated from its dependencies.

In an object-oriented scenario, class A depends on class B when any of the following conditions are verified:

  • Class A derives from class B.

  • Class A includes a member of class B.

  • One of the methods of class A invokes a method of class B.

  • One of the methods of class A receives or returns a parameter of class B.

  • Class A depends on a class that, in turn, depends on class B.

How can you neutralize dependencies when testing a method? This is exactly where manually written test harnesses no longer live up to your expectations, and you see the full power of automated testing frameworks.

Dependency injection really comes in handy here and is a pattern that has a huge impact on testability. A class that depends on interfaces (the first principle of OOD), and uses dependency injection to receive from the outside world any objects it needs to do its own work, is inherently more testable. Let’s consider the following code snippet:

public class Task
{
  // Class Task depends upon type ILogger
  ILogger _logger;

  public Task(ILogger logger)
  {
    this._logger = logger;
  }

  public int Sum(int x, int y)
  {
    return x+y;
  }
  public void Execute()
  {
    // Invoke an external "service"; not relevant when unit-testing this method
    this._logger.Log("Begin method ...");

    // Method specific code; RELEVANT when unit-testing this method
    .
    .
    .

    // Invoke an external "service"; not relevant when unit-testing this method
    this._logger.Log("End method ...");
  }
}

We want to test the code in method Execute, but we don’t care about the logger. Because the class Task is designed with DI in mind, testing the method Execute in total isolation is much easier.

Again, how can you neutralize dependencies when testing a method?

The simplest option is using fake objects. A fake object is a relatively simple clone of an object that offers the same interface as the original object but returns hard-coded or programmatically determined values. Here’s a sample fake object for the ILogger type:

public class FakeLogger : ILogger
{
    public void Log(string message)
    {
        return;
    }
}

As you can see, the behavior of a fake object is hard-coded; the fake object has no state and no significant behavior. From the fake object’s perspective, it makes no difference how many times you invoke a fake method and when in the flow the call occurs. Let’s see how to inject a fake logger in the Task class:

[TestMethod]
public void TestIfExecuteWorks()
{
  // Inject a fake logger to isolate the method from dependencies
  FakeLogger fake = new FakeLogger();
  Task task = new Task(fake);

  // Set preconditions
  int x = 3;
  int y = 4;
  int expected = 7;

  // Run the method
  int actual = task.Sum(x, y);

  // Report about the code's behavior using Assert statements
  Assert.AreEqual<int>(expected, actual);
  .
  .
  .
}

In a test, you set the preconditions for the method, run the method, and then observe the resulting postconditions. The concept of assertion is central to the unit test. An assertion is a condition that might or might not be verified. If verified, the assertion passes. In MSTest, the Assert class provides many static methods for making assertions, such as AreEqual, IsInstanceOfType, and IsNull.

In the preceding example, after executing the method Sum you are expected to place one or more assertions aimed at verifying the changes made to the state of the object or comparing the results produced against expected values.

From Fakes to Mocks

A mock object is a more evolved and recent version of a fake. A mock does all that a fake or a stub does, plus something more. In a way, a mock is an object with its own personality that mimics the behavior and interface of another object. What more does a mock provide to testers?

Essentially, a mock allows for verification of the context of the method call. With a mock, you can verify that a method call happens with the right preconditions and in the correct order with respect to other methods in the class.

Writing a fake manually is not usually a big issue—all the logic you need is for the most part simple and doesn’t need to change frequently. When you use fakes, you’re mostly interested in verifying that some expected output derives from a given input. You are interested in the state that a fake object might represent; you are not interested in interacting with it.

You use a mock instead of a fake only when you need to interact with dependent objects during tests. For example, you might want to know whether the mock has been invoked or not, and you might decide within the text what the mock object has to return for a given method.

Writing mocks manually is certainly a possibility, but is rarely an option you really want to consider. For the level of flexibility you expect from a mock, you should be updating its source code every now and then or you should have (and maintain) a different mock for each test case in which the object is being involved. Alternatively, you might come up with a very generic mock class that works in the guise of any object you specify. This very generic mock class also exposes a general-purpose interface through which you set your expectations for the mocked object. This is exactly what mocking frameworks do for you. In the end, you never write mock objects manually; you generate them on the fly using some mocking framework.

Table 3-3 lists and briefly describes the commonly used mocking frameworks.

Table 3-3. Some Popular Mocking Frameworks

Product

Description

NMock2

An open-source library providing a dynamic mocking framework for .NET interfaces. The mock object uses strings to get input and reflection to set expectations. Read more at http://sourceforge.net/projects/nmock2.

TypeMock

A commercial product with unique capabilities that basically don’t require you to (re)design your code for testability. TypeMock enables testing code that was previously considered untestable, such as static methods, nonvirtual methods, and sealed classes.

Read more at http://www.typemock.com.

Rhino Mocks

An open-source product. Through a wizard, it generates a static mock class for type-safe testing. You set mock expectations by accessing directly the mocked object, rather than going through one more level of indirection.

Read more at http://www.ayende.com/projects/rhino-mocks.aspx.

Let’s go through a mocking example that uses NMock2 in MSTest.

Imagine you have an AccountService class that depends on the ICurrencyService type. The AccountService class represents a bank account with its own currency. When you transfer funds between accounts, you might need to deal with conversion rates, and you use the ICurrencyService type for that:

public interface ICurrencyService
{
  // Returns the current conversion rate: how many "fromCurrency" to
  // be changed into toCurrency
  decimal GetConversionRate(string fromCurrency, string toCurrency);
}

Let’s see what testing the TransferFunds method looks like:

[TestClass]
public class CurrencyServiceTestCase
{
  private Mockery mocks;
  private ICurrencyService mockCurrencyService;
  private IAccountService accountService;
  [TestInitialize]
  public void SetUp()
  {
    // Initialize the mocking framework
    mocks = new Mockery();

    // Generate a mock for the ICurrencyService type
    mockCurrencyService = mocks.NewMock<ICurrencyService>();

    // Create the object to test and inject the mocked service
    accountService = new AccountService(mockCurrencyService);
  }

  [TestMethod]
  public void TestCrossCurrencyFundsTransfer()
  {
    // Create two test accounts
    Account eurAccount = new Account("12345", "EUR");
    Account usdAccount = new Account("54321", "USD");
    usdAccount.Deposit(1000);

    // Set expectations for the mocked object:
    //   When method GetConversionRate is invoked with (USD,EUR) input
    //   the mock returns 0.64
    Expect.Once.On(mockCurrencyService)
                .Method("GetConversionRate")
                .With("USD", "EUR")
                .Will(Return.Value(0.64));

    // Invoke the method to test (and transfer $500 to an EUR account)
    accountService.TransferFunds(usdAccount, eurAccount, 500);

    // Verify postconditions through assertions
    Assert.AreEqual<int>(500, usdAccount.Balance);
    Assert.AreEqual<int>(320, eurAccount.Balance);
    mocks.VerifyAllExpectationsHaveBeenMet();
  }
}

You first create a mock object for each dependent type. Next, you programmatically set expectations on the mock using the static class Expect from the NMock2 framework. In particular, in this case you establish that when the method GetConversionRate on the mocked type is invoked with a pair of arguments such as “USD” and “EUR”, it has to return 0.64. This is just the value that the method TransferFunds receives when it attempts to invoke the currency services internally.

There’s no code around that belongs to a mock object, and there’s no need for developers to look into the implementation of mocks. Reading a test, therefore, couldn’t be easier. The expectations are clearly declared and correctly passed on the methods under test.

Security

Located at Carnegie Mellon University in Pittsburgh, Pennsylvania, the CERT Coordination Center (CERT/CC) analyzes the current state of Internet security. It regularly receives reports of vulnerabilities and researches the inner causes of security vulnerabilities. The center’s purpose is to help with the development of secure coding practices.

Figure 3-4 shows a statistic about the number of identified vulnerabilities in the past ten years. As you can see, the trend is impressive. Also, you should consider that the data includes only the first two quarters of 2008. (See http://www.cert.org/stats/vulnerability_remediation.html.)

Figure 3-4

Figure 3-4. Identified security vulnerabilities in past ten years

It is broadly accepted that these numbers have a common root—they refer to software created through methodologies not specifically oriented to security. On the other hand, the problem of security is tightly related to the explosion in the popularity of the Internet. Only ten years ago, the big bubble was just a tiny balloon.

In sharp contrast with the ISO/IEC 9126 standard, all current methodologies for software development (agile, waterfall, MSF, and the like) hardly mention the word security. Additionally, the use of these methodologies has not resulted (yet?) in a measurable reduction of security bugs. To accomplish this, you need more than these methodologies offer.

Security as a (Strict) Requirement

We can’t really say whether this is a real story or an urban legend, but it’s being said that a few years ago, in the early days of the .NET Framework, a consultant went to some CIA office for a training gig. When introducing Code Access Security—the .NET Framework mechanism to limit access to code—the consultant asked students the following question: “Are you really serious about security here?”

Can you guess the answer? It was sort of like this: “Not only yes, but HELL YES. And you’ll experience that yourself when you attempt to get out of the building.”

Being serious about (software) security, though, is a subtle concept that goes far beyond even your best intentions. As Microsoft’s senior security program manager Michael Howard points out:

  • If your engineers know nothing about the basic security tenets, common security defect types, basic secure design, or security testing, there really is no reasonable chance they could produce secure software. I say this because, on the average, software engineers don’t pay enough attention to security. They may know quite a lot about security features, but they need to have a better understanding of what it takes to build and deliver secure features.

Security must be taken care of from the beginning. A secure design starts with the architecture; it can’t be something you bolt on at a later time. Security is by design. To address security properly, you need a methodology developed with security in mind that leads you to design your system with security in mind. This is just what the Security Development Lifecycle (SDL) is all about.

Security Development Lifecycle

SDL is a software development process that Microsoft uses internally to improve software security by reducing security bugs. SDL is not just an internal methodology. Based on the impressive results obtained internally, Microsoft is now pushing SDL out to any development team that wants to be really serious about security.

SDL is essentially an iterative process that focuses on security aspects of developing software. SDL doesn’t mandate a particular software development process and doesn’t preclude any. It is agnostic to the methodology in use in the project—be it waterfall, agile, spiral, or whatever else.

SDL is the incarnation of the SD3+C principle, which is a shortcut for “Secure by Design, Secure by Default, Secure in Deployment, plus Communication.” Secure by Design refers to identifying potential security risks starting with the design phase. Secure by Default refers to reducing the attack surface of each component and making it run with the least possible number of privileges. Secure in Deployment refers to making security requirements clear during deployment. Communication refers to sharing information about findings to apply a fix in a timely manner.

Foundations of SDL: Layering

The foundations of SDL are essentially three: layering, componentization, and roles.

Decomposing the architecture to layers is important because of the resulting separation of concerns. Having functionality organized in distinct layers makes it easier to map functions to physical tiers as appropriate. This is beneficial at various levels.

For example, it is beneficial for the data server.

You can isolate the data server at will, and even access it through a separate network. In this case, the data server is much less sensitive to denial of service (DoS) attacks because of the firewalls scattered along the way that can recognize and neutralize DoS packets.

You move all security checks to the business layer running on the application server and end up with a single user for the database—the data layer. Among other things, this results in a bit less work for the database and a pinch of additional scalability for the system.

Layers are beneficial for the application server, too.

You use Code Access Security (CAS) on the business components to stop untrusted code from executing privileged actions. You use CAS imperatively through xxxPermission classes to decide what to do based on actual permissions. You use CAS declaratively on classes or assemblies through xxxPermission attributes to prevent unauthorized use of sensitive components. If you have services, the contract helps to delimit what gets in and what gets out of the service.

Finally, if layering is coupled with thin clients, you have fewer upgrades (which are always a risk for the stability of the application) and less logic running on the client. Securitywise, this means that a possible dump of the client process would reveal much less information, so being able to use the client application in partial trust mode is more likely.

Foundations of SDL: Componentization

Each layer is decomposed to components. Components are organized by functions and required security privileges. It should be noted that performance considerations might lead you to grouping or further factorizing components in successive iterations.

Componentization here means identifying the components to secure and not merely breaking down the logical architecture into a group of assemblies.

For each component, you define the public contract and get to know exactly what data is expected to come in and out of the component. The decomposition can be hierarchical. From a security point of view, at this stage you are interested only in components within a layer that provide a service. You are not interested, for example, in the object model (that is, the domain model, typed DataSets, custom DTOs) because it is shared by multiple layers and represents only data and behavior on the data.

For each component, you identify the least possible set of privileges that make it run. From a security perspective, this means that in case of a successful attack, attackers gain the minimum possible set of privileges.

Components going to different processes run in total isolation and each has its own access control list (ACL) and Windows privileges set. Other components, conversely, might require their own AppDomain within the same .NET process. An AppDomain is like a virtual process within a .NET application that the Common Language Runtime (CLR) uses to isolate code within a secure boundary. (Note, however, that an AppDomain doesn’t represent a security barrier for applications running in full-trust mode.) An AppDomain can be sandboxed to have a limited set of permissions that, for example, limit disk access, socket access, and the like.

Foundation of SDL: Roles

Every application has its own assets. In general, an asset is any data that attackers might aim at, including a component with high privileges. Users access assets through the routes specified by use cases. From a security perspective, you should associate use cases with categories of users authorized to manage related assets.

A role is just a logical attribute assigned to a user. A role refers to the logical role the user plays in the context of the application. In terms of configuration, each user can be assigned one or more roles. This information is attached to the .NET identity object, and the application code can check it before the execution of critical operations. For example, an application might define two roles—Admin and Guest, each representative of a set of application-specific permissions. Users belonging to the Admin role can perform tasks that other users are prohibited from performing.

Assigning roles to a user account doesn’t add any security restrictions by itself. It is the responsibility of the application—typically, the business layer—to ensure that users perform only operations compatible with their role.

With roles, you employ a unique model for authorization, thus unifying heterogeneous security models such as LDAP, NTFS, database, and file system. Also, testing is easier. By impersonating a role, you can test access on any layer.

In a role-based security model, total risks related to the use of impersonation and delegation are mitigated. Impersonation allows a process to run using the security credentials of the impersonated user but, unlike delegation, it doesn’t allow access to remote resources on behalf of the impersonated user. In both cases, the original caller’s security context can be used to go through computer boundaries from the user interface to the middle tier and then all the way down to the database. This is a risk in a security model in which permissions are restricted by object. However, in a role-based security model, the ability to execute a method that accesses specific resources is determined by role membership, not credentials. User’s credentials might not be sufficient to operate on the application and data server.

Authorization Manager (AzMan) is a separate Windows download that enables you to group individual operations together to form tasks. You can then authorize roles to perform specific tasks, individual operations, or both. AzMan offers a centralized console (an MMC snap-in) to define manager roles, operations, and users.

Threat Model

Layering, componentization, and roles presuppose that, as an architect, you know the assets (such as sensitive data, highly privileged components) you want to protect from attackers. It also presupposes that you understand the threats related to the system you’re building and which vulnerabilities it might be exposed to after it is implemented. Design for security means that you develop a threat model, understand vulnerabilities, and do something to mitigate risks.

Ideally, you should not stop at designing this into your software, but look ahead to threats and vulnerabilities in the deployment environment and to those resulting from interaction with other products or systems. To this end, understanding the threats and developing a threat model is a must. For threats found at the design level, applying countermeasures is easy. Once the application has been developed, applying countermeasures is much harder. If an application is deployed, it’s nearly impossible to apply internal countermeasures—you have to rely on external security practices and devices. Therefore, it’s better to architect systems with built-in security features.

You can find an interesting primer on threat models at the following URL: http://blogs.msdn.com/ptorr/archive/2005/02/22/GuerillaThreatModelling.aspx.

Threat modeling essentially consists of examining components for different types of threats. STRIDE is a threat modeling practice that lists the following six types of threats:

  • Spoofing of user identity. Refers to using false identities to get into the system. This threat is mitigated by filtering out invalid IP addresses.

  • Tampering. Refers to intercepting/modifying data during a module’s conversation. This threat is mitigated by protecting the communication channel (for example, SSL or IPSec).

  • Repudiation. Refers to the execution of operations that can’t be traced back to the author. This threat is mitigated by strong auditing policies.

  • Information disclosure. Refers to unveiling private and sensitive information to unauthorized users. This threat is mitigated by enhanced authorization rules.

  • Denial of service. Refers to overloading a system up to the point of blocking it. This threat is mitigated by filtering out requests and frequently and carefully checking the use of the bandwidth.

  • Elevation of privilege. Refers to executing operations that require a higher privilege than the privilege currently assigned. This threat is mitigated by assigning the least possible privilege to any components.

If you’re looking for more information on STRIDE, you can check out the following URL: http://msdn.microsoft.com/en-us/magazine/cc163519.aspx.

After you have the complete list of threats that might apply to your application, you prioritize them based on the risks you see associated with each threat. It is not realistic, in fact, that you address all threats you find. Security doesn’t come for free, and you should balance costs with effectiveness. As a result, threats that you regard as unlikely or not particularly harmful can be given a lower priority or not covered at all.

How do you associate a risk with a threat? You use the DREAD model. It rates the risk as the probability of the attack multiplied by the impact it might have on the system. You should focus on the following aspects:

  • Discoverability. Refers to how high the likelihood is that an attacker discovers the vulnerability. It is a probability attribute.

  • Reproducibility. Refers to how easy it could be to replicate the attack. It is a probability attribute.

  • Exploitability. Refers to how easy it could be to perpetrate the attack. It is a probability attribute.

  • Affected users. Refers to the number of users affected by the attack. It is an impact attribute.

  • Damage potential. Refers to the quantity of damage the attack might produce. It is an impact attribute.

You typically use a simple High, Medium, or Low scale to determine the priority of the threats and decide which to address and when. If you’re looking for more information on DREAD, you can check out the following URL: http://msdn.microsoft.com/en-us/library/aa302419.aspx.

Security and the Architect

An inherently secure design, a good threat model, and a precise analysis of the risk might mean very little if you then pair them with a weak and insecure implementation. As an architect, you should intervene at three levels: development, code review, and testing.

As far as development is concerned, the use of strong typing should be enforced because, by itself, it cuts off a good share of possible bugs. Likewise, knowledge of common security patterns (for example, the “all input is evil” pattern), application of a good idiomatic design, and static code analysis (for example, using FxCop) are all practices to apply regularly and rigorously.

Sessions of code review should be dedicated to a careful examination of the actual configuration and implementation of security through CAS, and to spot the portions of code prone to amplified attacks, such as cross-site scripting, SQL injection, overflows, and similar attack mechanisms.

Unit testing for security is also important if your system receives files and sequences of bytes. You might want to consider a technique known as fuzzing. Fuzzing is a software testing technique through which you pass random data to a component as input. The code might throw an appropriate exception or degrade gracefully. However, it might also crash or fail some expected assertions. This technique can reveal some otherwise hidden bugs.

Final Security Push

Although security should be planned for from the outset, you can hardly make some serious security tests until the feature set is complete and the product is close to its beta stage. It goes without saying that any anomalies found during security tests lead the team to reconsidering the design and implementation of the application, and even the threat model.

The final security push before shipping to the customer is a delicate examination and should preferably be delegated to someone outside the team, preferably some other independent figure.

Releasing to production doesn’t mean the end of the security life cycle. As long as a system is up and running, it is exposed to possible attacks. You should always find time for penetration testing, which might lead to finding new vulnerabilities. So the team then starts the cycle again with the analysis of the design, implementation, and threat model. Over and over again, in an endless loop.